mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-11 18:22:00 -07:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dc975e8974 | ||
|
4311ade535 | ||
|
cd23401411 | ||
|
176ee6910f | ||
|
13c8f3d3aa | ||
|
ce9af687bc | ||
|
43f0d0cacd | ||
|
20b4e6953e | ||
|
7da287e3aa | ||
|
205f885d69 | ||
|
3715cd349d | ||
|
e4c3ecc57e | ||
|
673c5d886d | ||
|
f799b568d1 | ||
|
7bff4661f6 | ||
|
ffd8bef808 | ||
|
02cee2234d | ||
|
e0dd2be3fb | ||
|
a33c011c21 | ||
|
7c3f42bbba | ||
|
edac9820b5 | ||
|
84a47f7102 | ||
|
97ae8afb6f | ||
|
4138333f5c | ||
|
61339a8ae2 | ||
|
50eb2e3855 | ||
|
5fc78e4584 | ||
|
2736a2f69e | ||
|
179993f0cd | ||
|
b734f657f9 | ||
|
c29d7d02c2 | ||
|
3df6b2a58c | ||
|
b8aa2d2c32 | ||
|
0ff885461b | ||
|
ca43f95fb1 | ||
|
09700f676b | ||
|
4271e9cffa | ||
|
f3dc8a10d5 | ||
|
00fb486f6a | ||
|
4173e94c6f | ||
|
261d3d3340 | ||
|
15e20fcae1 | ||
|
f4f47f5fe3 | ||
|
71d11de7ca | ||
|
88d74a15aa | ||
|
0f02fc0c77 | ||
|
3f90fb42d8 | ||
|
9bd8988300 | ||
|
3c804bcfec | ||
|
cca4cdc4f1 | ||
|
8f899aaf8a | ||
|
e53b4bb439 | ||
|
ab247a1309 | ||
|
c21e9edad4 | ||
|
9c21a20f8b | ||
|
7191ebb615 | ||
|
a74731d7f5 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -17,6 +17,6 @@
|
||||
- [ ] bash
|
||||
- [ ] zsh
|
||||
- [ ] fish
|
||||
|
||||
|
||||
## Problem / Steps to reproduce
|
||||
|
||||
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
1
.github/workflows/codeql-analysis.yml
vendored
1
.github/workflows/codeql-analysis.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
1
.github/workflows/linux.yml
vendored
1
.github/workflows/linux.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
1
.github/workflows/macos.yml
vendored
1
.github/workflows/macos.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
44
ADVANCED.md
44
ADVANCED.md
@@ -149,7 +149,7 @@ fzf-tmux -p 80%,60%
|
||||
|
||||
> You might also want to check out my tmux plugins which support this popup
|
||||
> window layout.
|
||||
>
|
||||
>
|
||||
> - https://github.com/junegunn/tmux-fzf-url
|
||||
> - https://github.com/junegunn/tmux-fzf-maccy
|
||||
|
||||
@@ -190,7 +190,7 @@ list without restarting fzf.
|
||||
|
||||
### Toggling between data sources
|
||||
|
||||
You're not limiited to just one reload binding. Set up multiple bindings so
|
||||
You're not limited to just one reload binding. Set up multiple bindings so
|
||||
you can switch between data sources.
|
||||
|
||||
```sh
|
||||
@@ -349,7 +349,7 @@ IFS=: read -ra selected < <(
|
||||
fzf can kill the initial Ripgrep process it starts with the initial query.
|
||||
Otherwise, the initial Ripgrep process will keep consuming system resources
|
||||
even after `reload` is triggered.
|
||||
- Filtering is no longer a responsibitiliy of fzf; hence `--disabled`
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||
reduce the number of intermediate Ripgrep processes while we're typing in
|
||||
@@ -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}' "$@"
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
----------------------------
|
||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,6 +1,57 @@
|
||||
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
|
||||
`--preview` command is not given
|
||||
- Fixed bug where `{n}` is not properly reset on `reload`
|
||||
- Fixed bug where spinner is not displayed on `reload`
|
||||
- Enhancements in tcell renderer for Windows (#2616)
|
||||
- Vim plugin
|
||||
- `sinklist` is added as a synonym to `sink*` so that it's easier to add
|
||||
a function to a spec dictionary
|
||||
```vim
|
||||
let spec = { 'source': 'ls', 'options': ['--multi', '--preview', 'cat {}'] }
|
||||
function spec.sinklist(matches)
|
||||
echom string(a:matches)
|
||||
endfunction
|
||||
|
||||
call fzf#run(fzf#wrap(spec))
|
||||
```
|
||||
- Vim 7 compatibility
|
||||
|
||||
0.27.2
|
||||
------
|
||||
- 16 base ANSI colors can be specified by their names
|
||||
@@ -21,7 +72,7 @@ CHANGELOG
|
||||
" fzf will read the stream file while allowing other processes to append to it
|
||||
call fzf#run({'source': 'cat /dev/null > /tmp/stream; tail -f /tmp/stream'})
|
||||
```
|
||||
- It is now possible to open popup window relative to the currrent window
|
||||
- It is now possible to open popup window relative to the current window
|
||||
```vim
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||
```
|
||||
@@ -71,7 +122,7 @@ CHANGELOG
|
||||
```
|
||||
- Added `select` and `deselect` action for unconditionally selecting or
|
||||
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
||||
- Sigificant performance improvement in ANSI code processing
|
||||
- Significant performance improvement in ANSI code processing
|
||||
- Bug fixes and improvements
|
||||
- Built with Go 1.16
|
||||
|
||||
@@ -1153,4 +1204,3 @@ add `--sync` option to re-enable buffering.
|
||||
### Improvements
|
||||
- `--select-1` and `--exit-0` will start finder immediately when the condition
|
||||
cannot be met
|
||||
|
||||
|
@@ -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
|
||||
|
7
Makefile
7
Makefile
@@ -34,6 +34,7 @@ BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
@@ -59,6 +60,8 @@ else ifeq ($(UNAME_M),aarch64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),ppc64le)
|
||||
BINARY := $(BINARYPPC64LE)
|
||||
else ifeq ($(UNAME_M),riscv64)
|
||||
BINARY := $(BINARYRISCV64)
|
||||
else
|
||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||
endif
|
||||
@@ -99,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
|
||||
|
||||
@@ -141,6 +145,9 @@ target/$(BINARYARM8): $(SOURCES)
|
||||
target/$(BINARYPPC64LE): $(SOURCES)
|
||||
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYRISCV64): $(SOURCES)
|
||||
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
bin/fzf: target/$(BINARY) | bin
|
||||
cp -f target/$(BINARY) bin/fzf
|
||||
|
||||
|
@@ -283,7 +283,7 @@ The following table summarizes the available options.
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `options` | string/list | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||
@@ -387,7 +387,7 @@ command! -bang -complete=dir -nargs=? LS
|
||||
|
||||
- `g:fzf_layout`
|
||||
- `g:fzf_action`
|
||||
- **Works only when no custom `sink` (or `sink*`) is provided**
|
||||
- **Works only when no custom `sink` (or `sinklist`) is provided**
|
||||
- Having custom sink usually means that each entry is not an ordinary
|
||||
file path (e.g. name of color scheme), so we can't blindly apply the
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
|
@@ -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`)
|
||||
|
||||
[](https://repology.org/project/fzf/versions)
|
||||
|
||||
### Windows
|
||||
|
||||
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
||||
@@ -202,7 +204,7 @@ vim $(fzf)
|
||||
|
||||
#### Using the finder
|
||||
|
||||
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
|
||||
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
||||
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
|
||||
- Emacs style key bindings
|
||||
@@ -574,7 +576,7 @@ FZF_DEFAULT_COMMAND='find . -type f' \
|
||||
The following example uses fzf as the selector interface for ripgrep. We bound
|
||||
`reload` action to `change` event, so every time you type on fzf, the ripgrep
|
||||
process will restart with the updated query string denoted by the placeholder
|
||||
expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||
expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||
doesn't perform any secondary filtering.
|
||||
|
||||
```sh
|
||||
|
@@ -172,7 +172,7 @@ cleanup() {
|
||||
resize-pane -Z
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
if [[ $# -gt 0 ]]; then
|
||||
trap - EXIT
|
||||
exit 130
|
||||
fi
|
||||
|
@@ -300,7 +300,7 @@ The following table summarizes the available options.
|
||||
`source` | list | Vim list as input to fzf
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`options` | string/list | Options to fzf
|
||||
`dir` | string | Working directory
|
||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||
|
8
go.mod
8
go.mod
@@ -3,13 +3,13 @@ module github.com/junegunn/fzf
|
||||
require (
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
github.com/mattn/go-shellwords v1.0.11
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.2.0
|
||||
github.com/saracen/walker v0.1.2
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
18
go.sum
18
go.sum
@@ -5,14 +5,13 @@ github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404Z
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw=
|
||||
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
|
||||
@@ -21,10 +20,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.27.2
|
||||
version=0.29.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.27.2"
|
||||
$version="0.29.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
4
main.go
4
main.go
@@ -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() {
|
||||
|
@@ -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 "Jun 2021" "fzf 0.27.2" "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
|
||||
|
139
man/man1/fzf.1
139
man/man1/fzf.1
@@ -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 "Jun 2021" "fzf 0.27.2" "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)
|
||||
|
||||
|
@@ -159,7 +159,7 @@ function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = a:bin . ' --version'
|
||||
let command = a:bin . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
@@ -419,13 +419,13 @@ function! fzf#wrap(...)
|
||||
endif
|
||||
|
||||
" Action: g:fzf_action
|
||||
if !s:has_any(opts, ['sink', 'sink*'])
|
||||
if !s:has_any(opts, ['sink', 'sinklist', 'sink*'])
|
||||
let opts._action = get(g:, 'fzf_action', s:default_action)
|
||||
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
||||
function! opts.sink(lines) abort
|
||||
function! opts.sinklist(lines) abort
|
||||
return s:common_sink(self._action, a:lines)
|
||||
endfunction
|
||||
let opts['sink*'] = remove(opts, 'sink')
|
||||
let opts['sink*'] = opts.sinklist " For backward compatibility
|
||||
endif
|
||||
|
||||
return opts
|
||||
@@ -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,11 +521,12 @@ 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
|
||||
execute 'unlet $FZF_DEFAULT_COMMAND'
|
||||
let $FZF_DEFAULT_COMMAND = ''
|
||||
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
|
||||
endif
|
||||
endif
|
||||
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
|
||||
@@ -659,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')
|
||||
@@ -677,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
|
||||
@@ -876,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
|
||||
@@ -943,6 +950,8 @@ function! s:callback(dict, lines) abort
|
||||
endif
|
||||
if has_key(a:dict, 'sink*')
|
||||
call a:dict['sink*'](a:lines)
|
||||
elseif has_key(a:dict, 'sinklist')
|
||||
call a:dict['sinklist'](a:lines)
|
||||
endif
|
||||
catch
|
||||
if stridx(v:exception, ':E325:') < 0
|
||||
|
@@ -32,12 +32,12 @@ 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
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then
|
||||
elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then
|
||||
shift
|
||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
@@ -140,14 +140,14 @@ _fzf_handle_dynamic_completion() {
|
||||
orig_cmd="$1"
|
||||
orig_var="_fzf_orig_completion_$cmd"
|
||||
orig="${!orig_var##*#}"
|
||||
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
|
||||
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
elif [ -n "$_fzf_completion_loader" ]; then
|
||||
elif [[ -n "$_fzf_completion_loader" ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
@@ -171,17 +171,17 @@ __fzf_generic_path_completion() {
|
||||
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
||||
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [ -n "$matches" ]; then
|
||||
if [[ -n "$matches" ]]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
@@ -234,7 +234,7 @@ _fzf_complete() {
|
||||
|
||||
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [ -n "$selected" ]; then
|
||||
if [[ -n "$selected" ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
@@ -330,7 +330,7 @@ __fzf_defc() {
|
||||
opts="$3"
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var}"
|
||||
if [ -n "$orig" ]; then
|
||||
if [[ -n "$orig" ]]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
else
|
||||
|
@@ -27,7 +27,7 @@ __fzf_select__() {
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__fzfcmd() {
|
||||
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } &&
|
||||
[[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
@@ -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__() {
|
||||
@@ -52,7 +52,7 @@ __fzf_history__() {
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [ -z "$READLINE_POINT" ]; then
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
@@ -66,7 +66,7 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
|
@@ -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 ""
|
||||
|
@@ -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
|
||||
|
@@ -107,7 +107,7 @@ type Result struct {
|
||||
const (
|
||||
scoreMatch = 16
|
||||
scoreGapStart = -3
|
||||
scoreGapExtention = -1
|
||||
scoreGapExtension = -1
|
||||
|
||||
// We prefer matches at the beginning of a word, but the bonus should not be
|
||||
// too great to prevent the longer acronym matches from always winning over
|
||||
@@ -125,16 +125,16 @@ const (
|
||||
// Edge-triggered bonus for matches in camelCase words.
|
||||
// Compared to word-boundary case, they don't accompany single-character gaps
|
||||
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
||||
bonusCamel123 = bonusBoundary + scoreGapExtention
|
||||
bonusCamel123 = bonusBoundary + scoreGapExtension
|
||||
|
||||
// Minimum bonus point given to characters in consecutive chunks.
|
||||
// Note that bonus points for consecutive matches shouldn't have needed if we
|
||||
// used fixed match score as in the original algorithm.
|
||||
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
|
||||
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
|
||||
|
||||
// The first character in the typed pattern usually has more significance
|
||||
// than the rest so it's important that it appears at special positions where
|
||||
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||
// The amount of the extra bonus should be limited so that the gap penalty is
|
||||
// still respected.
|
||||
bonusFirstCharMultiplier = 2
|
||||
@@ -424,7 +424,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
} else {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
@@ -477,7 +477,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
var s1, s2, consecutive int16
|
||||
|
||||
if inGap {
|
||||
s2 = Hleft[off] + scoreGapExtention
|
||||
s2 = Hleft[off] + scoreGapExtension
|
||||
} else {
|
||||
s2 = Hleft[off] + scoreGapStart
|
||||
}
|
||||
@@ -598,7 +598,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
pidx++
|
||||
} else {
|
||||
if inGap {
|
||||
score += scoreGapExtention
|
||||
score += scoreGapExtension
|
||||
} else {
|
||||
score += scoreGapStart
|
||||
}
|
||||
|
@@ -43,10 +43,10 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
||||
for _, forward := range []bool{true, false} {
|
||||
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
@@ -54,18 +54,18 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
||||
scoreGapStart*2+scoreGapExtention*3)
|
||||
scoreGapStart*2+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||
@@ -73,13 +73,13 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
bonusNonWord+bonusBoundary)
|
||||
|
||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
||||
scoreGapStart*2+scoreGapExtention*4)
|
||||
scoreGapStart*2+scoreGapExtension*4)
|
||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtention*2)
|
||||
scoreGapStart*2+scoreGapExtension*2)
|
||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
||||
util.Max(bonusCamel123, bonusBoundary))
|
||||
@@ -99,7 +99,7 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
func TestFuzzyMatchBackward(t *testing.T) {
|
||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtention)
|
||||
scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -17,11 +16,13 @@ import (
|
||||
//
|
||||
// References:
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - http://ascii-table.com/ansi-escape-sequences.php
|
||||
// - http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
|
||||
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexRefence = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
@@ -36,7 +37,7 @@ func testParserReference(t testing.TB, str string) {
|
||||
s := str
|
||||
for i := 0; ; i++ {
|
||||
got := toSlice(nextAnsiEscapeSequence(s))
|
||||
exp := ansiRegexRefence.FindStringIndex(s)
|
||||
exp := ansiRegexReference.FindStringIndex(s)
|
||||
|
||||
equal := len(got) == len(exp)
|
||||
if equal {
|
||||
@@ -207,7 +208,7 @@ func TestExtractColor(t *testing.T) {
|
||||
if output != "hello world" {
|
||||
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
||||
}
|
||||
fmt.Println(src, ansiOffsets, clean)
|
||||
t.Log(src, ansiOffsets, clean)
|
||||
assertion(ansiOffsets, state)
|
||||
}
|
||||
|
||||
@@ -409,7 +410,7 @@ func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
a := ansiRegexRefence.FindStringIndex(s)
|
||||
a := ansiRegexReference.FindStringIndex(s)
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
|
@@ -233,6 +233,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
@@ -1592,7 +1642,7 @@ func validateSign(sign string, signOptName string) error {
|
||||
}
|
||||
|
||||
func postProcessOptions(opts *Options) {
|
||||
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
errorExit("--height option is currently not supported on this platform")
|
||||
}
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
@@ -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
|
||||
|
@@ -22,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
645
src/terminal.go
645
src/terminal.go
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
@@ -23,6 +24,26 @@ import (
|
||||
|
||||
// import "github.com/pkg/profile"
|
||||
|
||||
/*
|
||||
Placeholder regex is used to extract placeholders from fzf's template
|
||||
strings. Acts as input validation for parsePlaceholder function.
|
||||
Describes the syntax, but it is fairly lenient.
|
||||
|
||||
The following pseudo regex has been reverse engineered from the
|
||||
implementation. It is overly strict, but better describes whats possible.
|
||||
As such it is not useful for validation, but rather to generate test
|
||||
cases for example.
|
||||
|
||||
\\?(?: # escaped type
|
||||
{\+?s?f?RANGE(?:,RANGE)*} # token type
|
||||
|{q} # query type
|
||||
|{\+?n?f?} # item type (notice no mandatory element inside brackets)
|
||||
)
|
||||
RANGE = (?:
|
||||
(?:-?[0-9]+)?\.\.(?:-?[0-9]+)? # ellipsis syntax for token range (x..y)
|
||||
|-?[0-9]+ # shorthand syntax (x..x)
|
||||
)
|
||||
*/
|
||||
var placeholder *regexp.Regexp
|
||||
var whiteSuffix *regexp.Regexp
|
||||
var offsetComponentRegex *regexp.Regexp
|
||||
@@ -84,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 {
|
||||
@@ -193,6 +218,7 @@ const (
|
||||
reqRefresh
|
||||
reqReinit
|
||||
reqRedraw
|
||||
reqFullRedraw
|
||||
reqClose
|
||||
reqPrintQuery
|
||||
reqPreviewEnqueue
|
||||
@@ -263,6 +289,8 @@ const (
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actPreview
|
||||
actChangePreview
|
||||
actChangePreviewWindow
|
||||
actPreviewTop
|
||||
actPreviewBottom
|
||||
actPreviewUp
|
||||
@@ -301,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 {
|
||||
@@ -313,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)
|
||||
}
|
||||
@@ -393,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
|
||||
}
|
||||
}
|
||||
@@ -425,6 +454,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
delay = initialDelay
|
||||
}
|
||||
var previewBox *util.EventBox
|
||||
showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden
|
||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) {
|
||||
previewBox = util.NewEventBox()
|
||||
}
|
||||
@@ -472,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, previewBox != nil && !opts.Preview.hidden, 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)
|
||||
@@ -676,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
|
||||
}
|
||||
|
||||
@@ -953,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()
|
||||
@@ -980,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 {
|
||||
@@ -1038,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
|
||||
@@ -1143,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
|
||||
}
|
||||
@@ -1519,22 +1574,6 @@ func keyMatch(key tui.Event, event tui.Event) bool {
|
||||
key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
|
||||
}
|
||||
|
||||
func quoteEntryCmd(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
|
||||
})
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
if util.IsWindows() {
|
||||
return quoteEntryCmd(entry)
|
||||
}
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
}
|
||||
|
||||
func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
flags := placeholderFlags{}
|
||||
|
||||
@@ -1560,6 +1599,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
skipChars++
|
||||
case 'q':
|
||||
flags.query = true
|
||||
// query flag is not skipped
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -1609,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)
|
||||
@@ -1622,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 {
|
||||
@@ -1647,77 +1693,86 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
if selected[0] == nil {
|
||||
selected = []*Item{}
|
||||
}
|
||||
|
||||
// replace placeholders one by one
|
||||
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||
escaped, match, flags := parsePlaceholder(match)
|
||||
|
||||
if escaped {
|
||||
// this function implements the effects a placeholder has on items
|
||||
var replace func(*Item) string
|
||||
|
||||
// placeholder types (escaped, query type, item type, token type)
|
||||
switch {
|
||||
case escaped:
|
||||
return match
|
||||
case match == "{q}":
|
||||
return quoteEntry(query)
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
case flags.number:
|
||||
n := int(item.text.Index)
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(n)
|
||||
case flags.file:
|
||||
return item.AsString(stripAnsi)
|
||||
default:
|
||||
return quoteEntry(item.AsString(stripAnsi))
|
||||
}
|
||||
}
|
||||
default:
|
||||
// token type and also failover (below)
|
||||
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||
ranges := make([]Range, len(rangeExpressions))
|
||||
for idx, s := range rangeExpressions {
|
||||
r, ok := ParseRange(&s) // ellipsis (x..y) and shorthand (x..x) range syntax
|
||||
if !ok {
|
||||
// Invalid expression, just return the original string in the template
|
||||
return match
|
||||
}
|
||||
ranges[idx] = r
|
||||
}
|
||||
|
||||
replace = func(item *Item) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := joinTokens(trans)
|
||||
|
||||
// trim the last delimiter
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
// make sure the delimiter is at the very end of the string
|
||||
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
||||
str = str[:delims[len(delims)-1][0]]
|
||||
}
|
||||
}
|
||||
|
||||
if !flags.preserveSpace {
|
||||
str = strings.TrimSpace(str)
|
||||
}
|
||||
if !flags.file {
|
||||
str = quoteEntry(str)
|
||||
}
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
// Current query
|
||||
if match == "{q}" {
|
||||
return quoteEntry(query)
|
||||
}
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
items = selected
|
||||
}
|
||||
|
||||
replacements := make([]string, len(items))
|
||||
|
||||
if match == "{}" {
|
||||
for idx, item := range items {
|
||||
if flags.number {
|
||||
n := int(item.text.Index)
|
||||
if n < 0 {
|
||||
replacements[idx] = ""
|
||||
} else {
|
||||
replacements[idx] = strconv.Itoa(n)
|
||||
}
|
||||
} else if flags.file {
|
||||
replacements[idx] = item.AsString(stripAnsi)
|
||||
} else {
|
||||
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
|
||||
}
|
||||
}
|
||||
if flags.file {
|
||||
return writeTemporaryFile(replacements, printsep)
|
||||
}
|
||||
return strings.Join(replacements, " ")
|
||||
}
|
||||
|
||||
tokens := strings.Split(match[1:len(match)-1], ",")
|
||||
ranges := make([]Range, len(tokens))
|
||||
for idx, s := range tokens {
|
||||
r, ok := ParseRange(&s)
|
||||
if !ok {
|
||||
// Invalid expression, just return the original string in the template
|
||||
return match
|
||||
}
|
||||
ranges[idx] = r
|
||||
}
|
||||
|
||||
for idx, item := range items {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := joinTokens(trans)
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
||||
str = str[:delims[len(delims)-1][0]]
|
||||
}
|
||||
}
|
||||
if !flags.preserveSpace {
|
||||
str = strings.TrimSpace(str)
|
||||
}
|
||||
if !flags.file {
|
||||
str = quoteEntry(str)
|
||||
}
|
||||
replacements[idx] = str
|
||||
replacements[idx] = replace(item)
|
||||
}
|
||||
|
||||
if flags.file {
|
||||
return writeTemporaryFile(replacements, printsep)
|
||||
}
|
||||
@@ -1725,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()
|
||||
}
|
||||
@@ -1746,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)
|
||||
@@ -1891,7 +1948,7 @@ func (t *Terminal) Loop() {
|
||||
go func() {
|
||||
for {
|
||||
<-resizeChan
|
||||
t.reqBox.Set(reqRedraw, nil)
|
||||
t.reqBox.Set(reqFullRedraw, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -1930,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
|
||||
}
|
||||
@@ -1947,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())
|
||||
@@ -2086,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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2141,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() {
|
||||
@@ -2211,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()
|
||||
@@ -2254,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
|
||||
@@ -2267,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:
|
||||
@@ -2285,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})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2447,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)
|
||||
@@ -2478,7 +2539,7 @@ func (t *Terminal) Loop() {
|
||||
req(reqClose)
|
||||
}
|
||||
case actClearScreen:
|
||||
req(reqRedraw)
|
||||
req(reqFullRedraw)
|
||||
case actClearQuery:
|
||||
t.input = []rune{}
|
||||
t.cx = 0
|
||||
@@ -2627,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 {
|
||||
@@ -2657,12 +2718,53 @@ func (t *Terminal) Loop() {
|
||||
if valid {
|
||||
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
|
||||
}
|
||||
@@ -2670,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
|
||||
}
|
||||
@@ -2732,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) {
|
||||
|
@@ -1,18 +1,17 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func newItem(str string) *Item {
|
||||
bytes := []byte(str)
|
||||
trimmed, _, _ := extractColor(str, nil, nil)
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
@@ -30,75 +29,96 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
t.Errorf("expected: %s, actual: %s", expected, result)
|
||||
}
|
||||
}
|
||||
// helper function that converts template format into string and carries out the check()
|
||||
checkFormat := func(format string) {
|
||||
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
|
||||
unixStyle := quotes{`'`, `'\''`, "\n"}
|
||||
windowsStyle := quotes{`^"`, `'`, "\n"}
|
||||
var effectiveStyle quotes
|
||||
|
||||
if util.IsWindows() {
|
||||
effectiveStyle = windowsStyle
|
||||
} else {
|
||||
effectiveStyle = unixStyle
|
||||
}
|
||||
|
||||
expected := templateToString(format, effectiveStyle)
|
||||
check(expected)
|
||||
}
|
||||
printsep := "\n"
|
||||
|
||||
/*
|
||||
Test multiple placeholders and the function parameters.
|
||||
*/
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz' 'query'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with "'" delimiter
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'")
|
||||
checkFormat("echo {{.O}} foo{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo 'bar baz'")
|
||||
checkFormat("echo {{.O}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with regex delimiter
|
||||
regex = regexp.MustCompile(`\w+`)
|
||||
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' '")
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ''\\'''")
|
||||
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' '")
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
@@ -106,34 +126,513 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
check("echo /' foo'\\''bar baz'")
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||
|
||||
// Regex delimiter
|
||||
regex = regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
|
||||
|
||||
/*
|
||||
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
|
||||
see: TestParsePlaceholder
|
||||
*/
|
||||
items3 := []*Item{
|
||||
// single line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
// multi line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
}
|
||||
stripAnsi := false
|
||||
printsep = "\n"
|
||||
forcePlus := false
|
||||
query := "sample query"
|
||||
|
||||
templateToOutput := make(map[string]string)
|
||||
templateToFile := make(map[string]string) // same as above, but the file contents will be matched
|
||||
// I. item type placeholder
|
||||
templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
|
||||
templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
|
||||
templateToOutput[`{n}`] = `0`
|
||||
templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
|
||||
templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
|
||||
templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
|
||||
templateToFile[`{nf}`] = `0{{.S}}`
|
||||
templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
|
||||
|
||||
// II. token type placeholders
|
||||
templateToOutput[`{..}`] = templateToOutput[`{}`]
|
||||
templateToOutput[`{1..}`] = templateToOutput[`{}`]
|
||||
templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
|
||||
templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
|
||||
templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
|
||||
// shorthand for x..x range
|
||||
templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
|
||||
templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
|
||||
templateToOutput[`{-6}`] = templateToOutput[`{1}`]
|
||||
// multiple ranges
|
||||
templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
|
||||
templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
|
||||
templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||
templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||
// flags
|
||||
templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
|
||||
templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
|
||||
templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
|
||||
templateToFile[`{f1}`] = `1a{{.S}}`
|
||||
templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
|
||||
templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
|
||||
|
||||
// III. query type placeholder
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||
|
||||
// IV. escaping placeholder
|
||||
templateToOutput[`\{}`] = `{}`
|
||||
templateToOutput[`\{++}`] = `{++}`
|
||||
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||
|
||||
for giveTemplate, wantOutput := range templateToOutput {
|
||||
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
checkFormat(wantOutput)
|
||||
}
|
||||
for giveTemplate, wantOutput := range templateToFile {
|
||||
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
|
||||
data, err := readFile(path)
|
||||
if err != nil {
|
||||
t.Errorf("Cannot read the content of the temp file %s.", path)
|
||||
}
|
||||
result = string(data)
|
||||
|
||||
checkFormat(wantOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuoteEntryCmd(t *testing.T) {
|
||||
func TestQuoteEntry(t *testing.T) {
|
||||
type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
|
||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||
var effectiveStyle quotes
|
||||
|
||||
if util.IsWindows() {
|
||||
effectiveStyle = windowsStyle
|
||||
} else {
|
||||
effectiveStyle = unixStyle
|
||||
}
|
||||
|
||||
tests := map[string]string{
|
||||
`"`: `^"\^"^"`,
|
||||
`\`: `^"\\^"`,
|
||||
`\"`: `^"\\\^"^"`,
|
||||
`"\\\"`: `^"\^"\\\\\\\^"^"`,
|
||||
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
|
||||
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
|
||||
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
|
||||
`'`: `{{.O}}{{.SQ}}{{.O}}`,
|
||||
`"`: `{{.O}}{{.DQ}}{{.O}}`,
|
||||
`\`: `{{.O}}{{.BS}}{{.O}}`,
|
||||
`\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||
`"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||
|
||||
`$`: `{{.O}}${{.O}}`,
|
||||
`$HOME`: `{{.O}}$HOME{{.O}}`,
|
||||
`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
|
||||
|
||||
`&`: `{{.O}}{{.E}}&{{.O}}`,
|
||||
`|`: `{{.O}}{{.E}}|{{.O}}`,
|
||||
`<`: `{{.O}}{{.E}}<{{.O}}`,
|
||||
`>`: `{{.O}}{{.E}}>{{.O}}`,
|
||||
`(`: `{{.O}}{{.E}}({{.O}}`,
|
||||
`)`: `{{.O}}{{.E}}){{.O}}`,
|
||||
`@`: `{{.O}}{{.E}}@{{.O}}`,
|
||||
`^`: `{{.O}}{{.E}}^{{.O}}`,
|
||||
`%`: `{{.O}}{{.E}}%{{.O}}`,
|
||||
`!`: `{{.O}}{{.E}}!{{.O}}`,
|
||||
`%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
|
||||
`C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
|
||||
`"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
|
||||
}
|
||||
|
||||
for input, expected := range tests {
|
||||
escaped := quoteEntryCmd(input)
|
||||
escaped := quoteEntry(input)
|
||||
expected = templateToString(expected, effectiveStyle)
|
||||
if escaped != expected {
|
||||
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
|
||||
func TestUnixCommands(t *testing.T) {
|
||||
if util.IsWindows() {
|
||||
t.SkipNow()
|
||||
}
|
||||
tests := []testCase{
|
||||
// reference: give{template, query, items}, want{output OR match}
|
||||
|
||||
// 1) working examples
|
||||
|
||||
// paths that does not have to evaluated will work fine, when quoted
|
||||
{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
|
||||
{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
|
||||
{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
|
||||
|
||||
// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
|
||||
// quoting the tilde is required (to be treated as string)
|
||||
{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'`}},
|
||||
{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
|
||||
}
|
||||
testCommands(t, tests)
|
||||
}
|
||||
|
||||
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
|
||||
func TestWindowsCommands(t *testing.T) {
|
||||
if !util.IsWindows() {
|
||||
t.SkipNow()
|
||||
}
|
||||
tests := []testCase{
|
||||
// reference: give{template, query, items}, want{output OR match}
|
||||
|
||||
// 1) working examples
|
||||
|
||||
// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
|
||||
{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
|
||||
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
|
||||
// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
|
||||
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
|
||||
// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
|
||||
{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^"`}},
|
||||
|
||||
// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
|
||||
// cat: "C:\\test.txt: Invalid argument
|
||||
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
|
||||
// cat: "C:\\test.txt": Invalid argument
|
||||
{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
|
||||
|
||||
// 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.
|
||||
|
||||
Also since the parser assumes the input is matched with `placeholder` regex,
|
||||
the regex is tested here as well.
|
||||
*/
|
||||
func TestParsePlaceholder(t *testing.T) {
|
||||
// give, want pairs
|
||||
templates := map[string]string{
|
||||
// I. item type placeholder
|
||||
`{}`: `{}`,
|
||||
`{+}`: `{+}`,
|
||||
`{n}`: `{n}`,
|
||||
`{+n}`: `{+n}`,
|
||||
`{f}`: `{f}`,
|
||||
`{+nf}`: `{+nf}`,
|
||||
|
||||
// II. token type placeholders
|
||||
`{..}`: `{..}`,
|
||||
`{1..}`: `{1..}`,
|
||||
`{..2}`: `{..2}`,
|
||||
`{1..2}`: `{1..2}`,
|
||||
`{-2..-1}`: `{-2..-1}`,
|
||||
// shorthand for x..x range
|
||||
`{1}`: `{1}`,
|
||||
`{1..1}`: `{1..1}`,
|
||||
`{-6}`: `{-6}`,
|
||||
// multiple ranges
|
||||
`{1,2}`: `{1,2}`,
|
||||
`{1,2,4}`: `{1,2,4}`,
|
||||
`{1,2..4}`: `{1,2..4}`,
|
||||
`{1..2,-4..-3}`: `{1..2,-4..-3}`,
|
||||
// flags
|
||||
`{+1}`: `{+1}`,
|
||||
`{+-1}`: `{+-1}`,
|
||||
`{s1}`: `{s1}`,
|
||||
`{f1}`: `{f1}`,
|
||||
`{+s1..2}`: `{+s1..2}`,
|
||||
`{+sf1..2}`: `{+sf1..2}`,
|
||||
|
||||
// III. query type placeholder
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
`{q}`: `{qq}`,
|
||||
|
||||
// IV. escaping placeholder
|
||||
`\{}`: `{}`,
|
||||
`\{++}`: `{++}`,
|
||||
`{++}`: `{+}`,
|
||||
}
|
||||
|
||||
for giveTemplate, wantTemplate := range templates {
|
||||
if !placeholder.MatchString(giveTemplate) {
|
||||
t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
|
||||
continue
|
||||
}
|
||||
|
||||
_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
|
||||
gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
|
||||
|
||||
if gotTemplate != wantTemplate {
|
||||
t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities section */
|
||||
|
||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||
func newItem(str string) *Item {
|
||||
bytes := []byte(str)
|
||||
trimmed, _, _ := extractColor(str, nil, nil)
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
// Functions tested in this file require array of items (allItems). The array needs
|
||||
// to consist of at least two nils. This is helper function.
|
||||
func newItems(str ...string) []*Item {
|
||||
result := make([]*Item, util.Max(len(str), 2))
|
||||
for i, s := range str {
|
||||
result[i] = newItem(s)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// (for logging purposes)
|
||||
func (item *Item) String() string {
|
||||
return item.AsString(true)
|
||||
}
|
||||
|
||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||
func templateToString(format string, data interface{}) string {
|
||||
bb := &bytes.Buffer{}
|
||||
|
||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return bb.String()
|
||||
}
|
||||
|
||||
// ad hoc types for test cases
|
||||
type give struct {
|
||||
template string
|
||||
query string
|
||||
allItems []*Item
|
||||
}
|
||||
type want struct {
|
||||
/*
|
||||
Unix:
|
||||
The `want.output` string is supposed to be formatted for evaluation by
|
||||
`sh -c command` system call.
|
||||
|
||||
Windows:
|
||||
The `want.output` string is supposed to be formatted for evaluation by
|
||||
`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
|
||||
behaviour, which is more favourable for nesting (possibly escaped)
|
||||
special characters. This is the relevant section of `help cmd`:
|
||||
|
||||
...old behavior is to see if the first character is
|
||||
a quote character and if so, strip the leading character and
|
||||
remove the last quote character on the command line, preserving
|
||||
any text after the last quote character.
|
||||
*/
|
||||
output string // literal output
|
||||
match string // output is matched against this regex (when output is empty string)
|
||||
}
|
||||
type testCase struct {
|
||||
give
|
||||
want
|
||||
}
|
||||
|
||||
func testCommands(t *testing.T, tests []testCase) {
|
||||
// common test parameters
|
||||
delim := "\t"
|
||||
delimiter := Delimiter{str: &delim}
|
||||
printsep := ""
|
||||
stripAnsi := false
|
||||
forcePlus := false
|
||||
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholder(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
switch {
|
||||
case test.want.output != "":
|
||||
if gotOutput != test.want.output {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.output)
|
||||
}
|
||||
case test.want.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
|
||||
wantRegex := regexp.MustCompile(wantMatch)
|
||||
if !wantRegex.MatchString(gotOutput) {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.match)
|
||||
}
|
||||
default:
|
||||
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// naive encoder of placeholder flags
|
||||
func (flags placeholderFlags) encodePlaceholder() string {
|
||||
encoded := ""
|
||||
if flags.plus {
|
||||
encoded += "+"
|
||||
}
|
||||
if flags.preserveSpace {
|
||||
encoded += "s"
|
||||
}
|
||||
if flags.number {
|
||||
encoded += "n"
|
||||
}
|
||||
if flags.file {
|
||||
encoded += "f"
|
||||
}
|
||||
if flags.query {
|
||||
encoded += "q"
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
|
||||
// can be replaced with os.ReadFile() in go 1.16+
|
||||
func readFile(path string) ([]byte, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data := make([]byte, 0, 128)
|
||||
for {
|
||||
if len(data) >= cap(data) {
|
||||
d := append(data[:cap(data)], 0)
|
||||
data = d[:len(data)]
|
||||
}
|
||||
|
||||
n, err := file.Read(data[len(data):cap(data)])
|
||||
data = data[:len(data)+n]
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ package fzf
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -19,3 +20,7 @@ func notifyStop(p *os.Process) {
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
@@ -17,3 +19,27 @@ func notifyStop(p *os.Process) {
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
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) + "'"
|
||||
}
|
||||
}
|
||||
|
@@ -73,6 +73,9 @@ func (r *LightRenderer) initPlatform() error {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
|
@@ -120,8 +120,11 @@ func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
// handle the following as private members of FullscreenRenderer instance
|
||||
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||
var (
|
||||
_screen tcell.Screen
|
||||
_screen tcell.Screen
|
||||
_prevMouseButton tcell.ButtonMask
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) initScreen() {
|
||||
@@ -185,14 +188,48 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
// process mouse events:
|
||||
case *tcell.EventMouse:
|
||||
// mouse down events have zeroed buttons, so we can't use them
|
||||
// mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed buttons
|
||||
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||
x, y := ev.Position()
|
||||
button := ev.Buttons()
|
||||
mod := ev.Modifiers() != 0
|
||||
if button&tcell.WheelDown != 0 {
|
||||
|
||||
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||
_prevMouseButton = button
|
||||
drag := prevButton == button
|
||||
|
||||
switch {
|
||||
case button&tcell.WheelDown != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
} else if button&tcell.WheelUp != 0 {
|
||||
case button&tcell.WheelUp != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
} else if runtime.GOOS != "windows" {
|
||||
case button&tcell.Button1 != 0 && !drag:
|
||||
// all potential double click events put their 'line' coordinate in the clickY array
|
||||
// double click event has two conditions, temporal and spatial, the first is checked here
|
||||
now := time.Now()
|
||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
|
||||
// detect double clicks (also check for spatial condition)
|
||||
n := len(r.clickY)
|
||||
double := n > 1 && r.clickY[n-2] == r.clickY[n-1]
|
||||
if double {
|
||||
// make sure two consecutive double clicks require four clicks
|
||||
r.clickY = []int{}
|
||||
}
|
||||
|
||||
// fire single or double click event
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||
case button&tcell.Button2 != 0 && !drag:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||
case runtime.GOOS != "windows":
|
||||
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
// to consume them.
|
||||
@@ -223,9 +260,13 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// process keyboard:
|
||||
case *tcell.EventKey:
|
||||
mods := ev.Modifiers()
|
||||
none := mods == tcell.ModNone
|
||||
alt := (mods & tcell.ModAlt) > 0
|
||||
ctrl := (mods & tcell.ModCtrl) > 0
|
||||
shift := (mods & tcell.ModShift) > 0
|
||||
ctrlAlt := ctrl && alt
|
||||
altShift := alt && shift
|
||||
|
||||
keyfn := func(r rune) Event {
|
||||
if alt {
|
||||
return CtrlAltKey(r)
|
||||
@@ -233,6 +274,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
|
||||
}
|
||||
switch ev.Key() {
|
||||
// section 1: Ctrl+(Alt)+[a-z]
|
||||
case tcell.KeyCtrlA:
|
||||
return keyfn('a')
|
||||
case tcell.KeyCtrlB:
|
||||
@@ -248,7 +290,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyCtrlG:
|
||||
return keyfn('g')
|
||||
case tcell.KeyCtrlH:
|
||||
return keyfn('h')
|
||||
switch ev.Rune() {
|
||||
case 0:
|
||||
if ctrl {
|
||||
return Event{BSpace, 0, nil}
|
||||
}
|
||||
case rune(tcell.KeyCtrlH):
|
||||
switch {
|
||||
case ctrl:
|
||||
return keyfn('h')
|
||||
case alt:
|
||||
return Event{AltBS, 0, nil}
|
||||
case none, shift:
|
||||
return Event{BSpace, 0, nil}
|
||||
}
|
||||
}
|
||||
case tcell.KeyCtrlI:
|
||||
return keyfn('i')
|
||||
case tcell.KeyCtrlJ:
|
||||
@@ -285,20 +341,25 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return keyfn('y')
|
||||
case tcell.KeyCtrlZ:
|
||||
return keyfn('z')
|
||||
// section 2: Ctrl+[ \]_]
|
||||
case tcell.KeyCtrlSpace:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case tcell.KeyCtrlBackslash:
|
||||
return Event{CtrlBackSlash, 0, nil}
|
||||
case tcell.KeyCtrlRightSq:
|
||||
return Event{CtrlRightBracket, 0, nil}
|
||||
case tcell.KeyCtrlCarat:
|
||||
return Event{CtrlCaret, 0, nil}
|
||||
case tcell.KeyCtrlUnderscore:
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
// section 3: (Alt)+Backspace2
|
||||
case tcell.KeyBackspace2:
|
||||
if alt {
|
||||
return Event{AltBS, 0, nil}
|
||||
}
|
||||
return Event{BSpace, 0, nil}
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
case tcell.KeyUp:
|
||||
if altShift {
|
||||
return Event{AltSUp, 0, nil}
|
||||
@@ -344,6 +405,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Right, 0, nil}
|
||||
|
||||
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||
case tcell.KeyInsert:
|
||||
return Event{Insert, 0, nil}
|
||||
case tcell.KeyHome:
|
||||
@@ -356,10 +418,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{PgUp, 0, nil}
|
||||
case tcell.KeyPgDn:
|
||||
return Event{PgDn, 0, nil}
|
||||
|
||||
case tcell.KeyBacktab:
|
||||
return Event{BTab, 0, nil}
|
||||
|
||||
case tcell.KeyF1:
|
||||
return Event{F1, 0, nil}
|
||||
case tcell.KeyF2:
|
||||
@@ -385,20 +445,31 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyF12:
|
||||
return Event{F12, 0, nil}
|
||||
|
||||
// ev.Ch doesn't work for some reason for space:
|
||||
// section 6: (Ctrl+Alt)+'rune'
|
||||
case tcell.KeyRune:
|
||||
r := ev.Rune()
|
||||
if alt {
|
||||
return AltKey(r)
|
||||
}
|
||||
return Event{Rune, r, nil}
|
||||
|
||||
switch {
|
||||
// translate native key events to ascii control characters
|
||||
case r == ' ' && ctrl:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
// handle AltGr characters
|
||||
case ctrlAlt:
|
||||
return Event{Rune, r, nil} // dropping modifiers
|
||||
// simple characters (possibly with modifier)
|
||||
case alt:
|
||||
return AltKey(r)
|
||||
default:
|
||||
return Event{Rune, r, nil}
|
||||
}
|
||||
|
||||
// section 7: Esc
|
||||
case tcell.KeyEsc:
|
||||
return Event{ESC, 0, nil}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// section 8: Invalid
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
|
392
src/tui/tcell_test.go
Normal file
392
src/tui/tcell_test.go
Normal file
@@ -0,0 +1,392 @@
|
||||
// +build tcell windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||
if got == want {
|
||||
return true
|
||||
} else {
|
||||
t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Test the handling of the tcell keyboard events.
|
||||
func TestGetCharEventKey(t *testing.T) {
|
||||
if util.ToTty() {
|
||||
// This test is skipped when output goes to terminal, because it causes
|
||||
// some glitches:
|
||||
// - output lines may not start at the beginning of a row which makes
|
||||
// the output unreadable
|
||||
// - terminal may get cleared which prevents you from seeing results of
|
||||
// previous tests
|
||||
// Good ways to prevent the glitches are piping the output to a pager
|
||||
// or redirecting to a file. I've found `less +G` to be trouble-free.
|
||||
t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.")
|
||||
} else if testing.Verbose() {
|
||||
// I have observed a behaviour when this test outputted more than 8192
|
||||
// bytes (32*256) into the 'less' pager, both the go's test executable
|
||||
// and the pager hanged. The go's executable was blocking on printing.
|
||||
// I was able to create minimal working example of that behaviour, but
|
||||
// that example hanged after 12256 bytes (32*(256+127)).
|
||||
t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.")
|
||||
}
|
||||
|
||||
if !HasFullscreenRenderer() {
|
||||
t.Skip("Can't test FullscreenRenderer.")
|
||||
}
|
||||
|
||||
// construct test cases
|
||||
type giveKey struct {
|
||||
Type tcell.Key
|
||||
Char rune
|
||||
Mods tcell.ModMask
|
||||
}
|
||||
type wantKey = Event
|
||||
type testCase struct {
|
||||
giveKey
|
||||
wantKey
|
||||
}
|
||||
/*
|
||||
Some test cases are marked "fabricated". It means that giveKey value
|
||||
is valid, but it is not what you get when you press the keys. For
|
||||
example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX
|
||||
(End-Of-Text character, causing SIGINT).
|
||||
I was trying to accompany the fabricated test cases with real ones.
|
||||
|
||||
Some test cases are marked "unhandled". It means that giveKey.Type
|
||||
is not present in tcell.go source code. It can still be handled via
|
||||
implicit or explicit alias.
|
||||
|
||||
If not said otherwise, test cases are for US keyboard.
|
||||
|
||||
(tabstop=44)
|
||||
*/
|
||||
tests := []testCase{
|
||||
|
||||
// section 1: Ctrl+(Alt)+[a-z]
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}},
|
||||
{giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C)
|
||||
{giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated
|
||||
// KeyTab is alias for KeyTAB
|
||||
{giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||
// KeyEnter is alias for KeyCR
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
// Ctrl+Alt keys
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
|
||||
// section 2: Ctrl+[ \]_]
|
||||
{giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' '
|
||||
{giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}},
|
||||
{giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}},
|
||||
{giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke
|
||||
{giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}},
|
||||
|
||||
// section 3: (Alt)+Backspace2
|
||||
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
|
||||
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
|
||||
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
|
||||
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
|
||||
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
|
||||
// section 6: (Ctrl+Alt)+'rune'
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}},
|
||||
{giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}},
|
||||
{giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}},
|
||||
/*
|
||||
"Input method" in Windows Language options:
|
||||
US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range
|
||||
CS: "Czech keyboard"
|
||||
DE: "German keyboard"
|
||||
|
||||
Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`.
|
||||
*/
|
||||
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level
|
||||
{giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$"
|
||||
{giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~"
|
||||
{giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char
|
||||
|
||||
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{"
|
||||
{giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@"
|
||||
{giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ"
|
||||
|
||||
// section 7: Esc
|
||||
// KeyEsc and KeyEscape are aliases for KeyESC
|
||||
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
|
||||
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
|
||||
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
|
||||
// section 8: Invalid
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}},
|
||||
{giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF
|
||||
{giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||
|
||||
}
|
||||
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
||||
r.Init()
|
||||
|
||||
// run and evaluate the tests
|
||||
for _, test := range tests {
|
||||
// generate key event
|
||||
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
|
||||
_screen.PostEventWait(giveEvent)
|
||||
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||
|
||||
// process the event in fzf and evaluate the test
|
||||
gotEvent := r.GetChar()
|
||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||
for gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||
|
||||
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
||||
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
}
|
||||
|
||||
/*
|
||||
Quick reference
|
||||
---------------
|
||||
|
||||
(tabstop=18)
|
||||
(this is not mapping table, it merely puts multiple constants ranges in one table)
|
||||
|
||||
¹) the two columns are each other implicit alias
|
||||
²) explicit aliases here
|
||||
|
||||
%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods
|
||||
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||
0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone
|
||||
1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift
|
||||
2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl
|
||||
3 1 KeyCtrlC KeyETX = ^C CtrlC
|
||||
4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt
|
||||
5 1 KeyCtrlE KeyENQ = ^E CtrlE
|
||||
6 1 KeyCtrlF KeyACK = ^F CtrlF
|
||||
7 1 KeyCtrlG KeyBEL = ^G CtrlG
|
||||
8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta
|
||||
9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab
|
||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||
17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ
|
||||
18 1 KeyCtrlR KeyDC2 = ^R CtrlR
|
||||
19 1 KeyCtrlS KeyDC3 = ^S CtrlS
|
||||
20 1 KeyCtrlT KeyDC4 = ^T CtrlT
|
||||
21 1 KeyCtrlU KeyNAK = ^U CtrlU
|
||||
22 1 KeyCtrlV KeySYN = ^V CtrlV
|
||||
23 1 KeyCtrlW KeyETB = ^W CtrlW
|
||||
24 1 KeyCtrlX KeyCAN = ^X CtrlX
|
||||
25 1 KeyCtrlY KeyEM = ^Y CtrlY
|
||||
26 1 KeyCtrlZ KeySUB = ^Z CtrlZ
|
||||
27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC
|
||||
28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace
|
||||
29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash
|
||||
30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket
|
||||
31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret
|
||||
32 CtrlSlash
|
||||
33 Invalid
|
||||
34 Resize
|
||||
35 Mouse
|
||||
36 DoubleClick
|
||||
37 LeftClick
|
||||
38 RightClick
|
||||
39 BTab
|
||||
40 BSpace
|
||||
41 Del
|
||||
42 PgUp
|
||||
43 PgDn
|
||||
44 Up
|
||||
45 Down
|
||||
46 Left
|
||||
47 Right
|
||||
48 Home
|
||||
49 End
|
||||
50 Insert
|
||||
51 SUp
|
||||
52 SDown
|
||||
53 SLeft
|
||||
54 SRight
|
||||
55 F1
|
||||
56 F2
|
||||
57 F3
|
||||
58 F4
|
||||
59 F5
|
||||
60 F6
|
||||
61 F7
|
||||
62 F8
|
||||
63 F9
|
||||
64 F10
|
||||
65 F11
|
||||
66 F12
|
||||
67 Change
|
||||
68 BackwardEOF
|
||||
69 AltBS
|
||||
70 AltUp
|
||||
71 AltDown
|
||||
72 AltLeft
|
||||
73 AltRight
|
||||
74 AltSUp
|
||||
75 AltSDown
|
||||
76 AltSLeft
|
||||
77 AltSRight
|
||||
78 Alt
|
||||
79 CtrlAlt
|
||||
..
|
||||
127 3 KeyDEL KeyBackspace2
|
||||
..
|
||||
256 6 KeyRune
|
||||
257 4 KeyUp
|
||||
258 4 KeyDown
|
||||
259 4 KeyRight
|
||||
260 4 KeyLeft
|
||||
261 8 KeyUpLeft
|
||||
262 8 KeyUpRight
|
||||
263 8 KeyDownLeft
|
||||
264 8 KeyDownRight
|
||||
265 8 KeyCenter
|
||||
266 5 KeyPgUp
|
||||
267 5 KeyPgDn
|
||||
268 5 KeyHome
|
||||
269 5 KeyEnd
|
||||
270 5 KeyInsert
|
||||
271 5 KeyDelete
|
||||
272 8 KeyHelp
|
||||
273 8 KeyExit
|
||||
274 8 KeyClear
|
||||
275 8 KeyCancel
|
||||
276 8 KeyPrint
|
||||
277 8 KeyPause
|
||||
278 5 KeyBacktab
|
||||
279 5 KeyF1
|
||||
280 5 KeyF2
|
||||
281 5 KeyF3
|
||||
282 5 KeyF4
|
||||
283 5 KeyF5
|
||||
284 5 KeyF6
|
||||
285 5 KeyF7
|
||||
286 5 KeyF8
|
||||
287 5 KeyF9
|
||||
288 5 KeyF10
|
||||
289 5 KeyF11
|
||||
290 5 KeyF12
|
||||
291 8 KeyF13
|
||||
292 8 KeyF14
|
||||
293 8 KeyF15
|
||||
294 8 KeyF16
|
||||
295 8 KeyF17
|
||||
296 8 KeyF18
|
||||
297 8 KeyF19
|
||||
298 8 KeyF20
|
||||
299 8 KeyF21
|
||||
300 8 KeyF22
|
||||
301 8 KeyF23
|
||||
302 8 KeyF24
|
||||
303 8 KeyF25
|
||||
304 8 KeyF26
|
||||
305 8 KeyF27
|
||||
306 8 KeyF28
|
||||
307 8 KeyF29
|
||||
308 8 KeyF30
|
||||
309 8 KeyF31
|
||||
310 8 KeyF32
|
||||
311 8 KeyF33
|
||||
312 8 KeyF34
|
||||
313 8 KeyF35
|
||||
314 8 KeyF36
|
||||
315 8 KeyF37
|
||||
316 8 KeyF38
|
||||
317 8 KeyF39
|
||||
318 8 KeyF40
|
||||
319 8 KeyF41
|
||||
320 8 KeyF42
|
||||
321 8 KeyF43
|
||||
322 8 KeyF44
|
||||
323 8 KeyF45
|
||||
324 8 KeyF46
|
||||
325 8 KeyF47
|
||||
326 8 KeyF48
|
||||
327 8 KeyF49
|
||||
328 8 KeyF50
|
||||
329 8 KeyF51
|
||||
330 8 KeyF52
|
||||
331 8 KeyF53
|
||||
332 8 KeyF54
|
||||
333 8 KeyF55
|
||||
334 8 KeyF56
|
||||
335 8 KeyF57
|
||||
336 8 KeyF58
|
||||
337 8 KeyF59
|
||||
338 8 KeyF60
|
||||
339 8 KeyF61
|
||||
340 8 KeyF62
|
||||
341 8 KeyF63
|
||||
342 8 KeyF64
|
||||
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||
%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods
|
||||
*/
|
@@ -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)
|
||||
@@ -117,11 +117,16 @@ func DurWithin(
|
||||
return val
|
||||
}
|
||||
|
||||
// IsTty returns true is stdin is a terminal
|
||||
// IsTty returns true if stdin is a terminal
|
||||
func IsTty() bool {
|
||||
return isatty.IsTerminal(os.Stdin.Fd())
|
||||
}
|
||||
|
||||
// ToTty returns true if stdout is a terminal
|
||||
func ToTty() bool {
|
||||
return isatty.IsTerminal(os.Stdout.Fd())
|
||||
}
|
||||
|
||||
// Once returns a function that returns the specified boolean value only once
|
||||
func Once(nextResponse bool) func() bool {
|
||||
state := nextResponse
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
155
test/test_go.rb
155
test/test_go.rb
@@ -2053,6 +2053,161 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys 'dabcd'
|
||||
tmux.until { |lines| assert_equal '> abcd', lines[-1] }
|
||||
end
|
||||
|
||||
def test_item_index_reset_on_reload
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_includes lines[1], '[[0]]'
|
||||
end
|
||||
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
|
||||
|
@@ -104,10 +104,10 @@ if [ -d "${fish_dir}/functions" ]; then
|
||||
remove "${fish_dir}/functions/fzf.fish"
|
||||
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
|
||||
if [ "$(ls -A "${fish_dir}/functions")" ]; then
|
||||
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
|
||||
else
|
||||
if [ -z "$(ls -A "${fish_dir}/functions")" ]; then
|
||||
rmdir "${fish_dir}/functions"
|
||||
else
|
||||
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
|
||||
fi
|
||||
fi
|
||||
|
||||
|
Reference in New Issue
Block a user