mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-14 14:23:47 -05:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e7259910 | ||
|
|
f0bfeba733 | ||
|
|
c3a7a24eea | ||
|
|
bbbcd780c9 | ||
|
|
475469a2e7 | ||
|
|
3a7447dcb6 | ||
|
|
e5d8cbd383 | ||
|
|
3c08dca7e7 | ||
|
|
d083f01d22 | ||
|
|
68cf393644 | ||
|
|
18f7230662 | ||
|
|
728f735281 | ||
|
|
ecc418ba77 | ||
|
|
3af5b7f2ac | ||
|
|
7a7cfcacbe | ||
|
|
52594355bf | ||
|
|
0d06c28b19 | ||
|
|
ccc4677252 | ||
|
|
821fc9feed | ||
|
|
82b46726fc | ||
|
|
8df872a482 | ||
|
|
c79c306adb | ||
|
|
51fdaad002 | ||
|
|
885cd8ff04 | ||
|
|
2707af403a | ||
|
|
2d227e5222 | ||
|
|
70529878e2 | ||
|
|
3b7a962dc6 | ||
|
|
6dcf5c3d7d | ||
|
|
b089bb5e7b | ||
|
|
a91a67668e | ||
|
|
220a908118 | ||
|
|
54841248e7 | ||
|
|
a0b42e6538 | ||
|
|
3312cf525d | ||
|
|
2093667548 | ||
|
|
3c868d7961 | ||
|
|
707f4f5816 | ||
|
|
b3ab6311c5 | ||
|
|
d56f605b63 | ||
|
|
f8b713f425 | ||
|
|
5209e95bc7 | ||
|
|
ef67a45702 | ||
|
|
b88eb72ac2 | ||
|
|
32847f7254 | ||
|
|
71df93b534 | ||
|
|
bb028191f8 | ||
|
|
19af8fc7d8 | ||
|
|
a06671b47f | ||
|
|
5f385d88e0 | ||
|
|
9cb7a364a3 | ||
|
|
f68cbc577d | ||
|
|
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/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]
|
||||
github: junegunn
|
||||
|
||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
16
.github/workflows/codeql-analysis.yml
vendored
16
.github/workflows/codeql-analysis.yml
vendored
@@ -6,9 +6,17 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
actions: read # for github/codeql-action/init to get workflow details
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/autobuild to send a status report
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -19,18 +27,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@1ed1437484560351c5be56cf73a48a279d116b78
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@1ed1437484560351c5be56cf73a48a279d116b78
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@1ed1437484560351c5be56cf73a48a279d116b78
|
||||
|
||||
15
.github/workflows/linux.yml
vendored
15
.github/workflows/linux.yml
vendored
@@ -6,25 +6,26 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: 1.18
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
|
||||
15
.github/workflows/macos.yml
vendored
15
.github/workflows/macos.yml
vendored
@@ -6,25 +6,26 @@ on:
|
||||
branches: [ master, devel ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: 1.18
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
uses: ruby/setup-ruby@ebaea52cb20fea395b0904125276395e37183dac
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ builds:
|
||||
post: |
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
|
||||
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
golang 1.18
|
||||
77
ADVANCED.md
77
ADVANCED.md
@@ -17,6 +17,7 @@ Advanced fzf examples
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
@@ -190,7 +191,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 +350,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
|
||||
@@ -405,6 +406,40 @@ IFS=: read -ra selected < <(
|
||||
- We reverted `--color` option for customizing how the matching chunks are
|
||||
displayed in the second phase
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode
|
||||
|
||||
*(Requires fzf 0.30.0 or above)*
|
||||
|
||||
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
|
||||
previously "unbound" via `unbind`.
|
||||
|
||||
This is an improved version of the previous example that allows us to switch
|
||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||
CTRL-F.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
|
||||
--prompt '1. Ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||
)
|
||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -429,30 +464,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
|
||||
----------------------------
|
||||
|
||||
2
BUILD.md
2
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.13 or above
|
||||
- Go 1.17 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
|
||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,6 +1,126 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.32.0
|
||||
------
|
||||
- Updated the scoring algorithm
|
||||
- Different bonus points to different categories of word boundaries
|
||||
(listed higher to lower bonus point)
|
||||
- Word after whitespace characters or beginning of the string
|
||||
- Word after common delimiter characters (`/,:;|`)
|
||||
- Word after other non-word characters
|
||||
```sh
|
||||
# foo/bar.sh` is preferred over `foo-bar.sh` on `bar`
|
||||
fzf --query=bar --height=4 << EOF
|
||||
foo-bar.sh
|
||||
foo/bar.sh
|
||||
EOF
|
||||
```
|
||||
- Added a new tiebreak `chunk`
|
||||
- Favors the line with shorter matched chunk. A chunk is a set of
|
||||
consecutive non-whitespace characters.
|
||||
- Unlike the default `length`, this scheme works well with tabular input
|
||||
```sh
|
||||
# length prefers item #1, because the whole line is shorter,
|
||||
# chunk prefers item #2, because the matched chunk ("foo") is shorter
|
||||
fzf --height=6 --header-lines=2 --tiebreak=chunk --reverse --query=fo << "EOF"
|
||||
N | Field1 | Field2 | Field3
|
||||
- | ------ | ------ | ------
|
||||
1 | hello | foobar | baz
|
||||
2 | world | foo | bazbaz
|
||||
EOF
|
||||
```
|
||||
- If the input does not contain any spaces, `chunk` is equivalent to
|
||||
`length`. But we're not going to set it as the default because it is
|
||||
computationally more expensive.
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.31.0
|
||||
------
|
||||
- Added support for an alternative preview window layout that is activated
|
||||
when the size of the preview window is smaller than a certain threshold.
|
||||
```sh
|
||||
# If the width of the preview window is smaller than 50 columns,
|
||||
# it will be displayed above the search window.
|
||||
fzf --preview 'cat {}' --preview-window 'right,50%,border-left,<50(up,30%,border-bottom)'
|
||||
|
||||
# Or you can just hide it like so
|
||||
fzf --preview 'cat {}' --preview-window '<50(hidden)'
|
||||
```
|
||||
- fzf now uses SGR mouse mode to properly support mouse on larger terminals
|
||||
- You can now use characters that do not satisfy `unicode.IsGraphic` constraint
|
||||
for `--marker`, `--pointer`, and `--ellipsis`. Allows Nerd Fonts and stuff.
|
||||
Use at your own risk.
|
||||
- Bug fixes and improvements
|
||||
- Shell extension
|
||||
- `kill` completion now requires trigger sequence (`**`) for consistency
|
||||
|
||||
0.30.0
|
||||
------
|
||||
- Fixed cursor flickering over the screen by hiding it during rendering
|
||||
- Added `--ellipsis` option. You can take advantage of it to make fzf
|
||||
effectively search non-visible parts of the item.
|
||||
```sh
|
||||
# Search against hidden line numbers on the far right
|
||||
nl /usr/share/dict/words |
|
||||
awk '{printf "%s%1000s\n", $2, $1}' |
|
||||
fzf --nth=-1 --no-hscroll --ellipsis='' |
|
||||
awk '{print $2}'
|
||||
```
|
||||
- Added `rebind` action for restoring bindings after `unbind`
|
||||
- Bug fixes and improvements
|
||||
|
||||
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 +141,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 +191,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 +1273,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
|
||||
|
||||
13
Makefile
13
Makefile
@@ -34,6 +34,8 @@ BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||
BINARYLOONG64 := fzf-$(GOOS)_loong64
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
@@ -59,6 +61,10 @@ else ifeq ($(UNAME_M),aarch64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),ppc64le)
|
||||
BINARY := $(BINARYPPC64LE)
|
||||
else ifeq ($(UNAME_M),riscv64)
|
||||
BINARY := $(BINARYRISCV64)
|
||||
else ifeq ($(UNAME_M),loongarch64)
|
||||
BINARY := $(BINARYLOONG64)
|
||||
else
|
||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||
endif
|
||||
@@ -99,6 +105,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 +148,12 @@ target/$(BINARYARM8): $(SOURCES)
|
||||
target/$(BINARYPPC64LE): $(SOURCES)
|
||||
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYRISCV64): $(SOURCES)
|
||||
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYLOONG64): $(SOURCES)
|
||||
GOARCH=loong64 $(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)
|
||||
|
||||
18
README.md
18
README.md
@@ -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
|
||||
@@ -380,12 +382,11 @@ cd ~/github/fzf**<TAB>
|
||||
|
||||
#### Process IDs
|
||||
|
||||
Fuzzy completion for PIDs is provided for kill command. In this case,
|
||||
there is no trigger sequence; just press the tab key after the kill command.
|
||||
Fuzzy completion for PIDs is provided for kill command.
|
||||
|
||||
```sh
|
||||
# Can select multiple processes with <TAB> or <Shift-TAB> keys
|
||||
kill -9 <TAB>
|
||||
kill -9 **<TAB>
|
||||
```
|
||||
|
||||
#### Host names
|
||||
@@ -556,7 +557,7 @@ more details.
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='ps -ef' \
|
||||
fzf --bind 'ctrl-r:reload($FZF_DEFAULT_COMMAND)' \
|
||||
fzf --bind 'ctrl-r:reload(eval "$FZF_DEFAULT_COMMAND")' \
|
||||
--header 'Press CTRL-R to reload' --header-lines=1 \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
@@ -565,7 +566,7 @@ FZF_DEFAULT_COMMAND='ps -ef' \
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='find . -type f' \
|
||||
fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload($FZF_DEFAULT_COMMAND)' \
|
||||
fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload(eval "$FZF_DEFAULT_COMMAND")' \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
@@ -590,6 +591,9 @@ If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
|
||||
and fzf will warn you about it. To suppress the warning message, we added
|
||||
`|| true` to the command, so that it always exits with 0.
|
||||
|
||||
See ["Using fzf as interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher)
|
||||
for a fuller example with preview window options.
|
||||
|
||||
### Preview window
|
||||
|
||||
When the `--preview` option is set, fzf automatically starts an external process
|
||||
|
||||
@@ -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%` )
|
||||
|
||||
22
go.mod
22
go.mod
@@ -2,16 +2,20 @@ 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/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
)
|
||||
|
||||
go 1.13
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
|
||||
29
go.sum
29
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,13 +20,13 @@ 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/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/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM=
|
||||
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
9
install
9
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.27.2
|
||||
version=0.32.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -256,7 +256,7 @@ for shell in $shells; do
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
@@ -280,11 +280,6 @@ EOF
|
||||
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
||||
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
|
||||
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
|
||||
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
|
||||
fi
|
||||
|
||||
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
if [ $key_bindings -ne 0 ]; then
|
||||
echo -n "Symlink $fish_binding ... "
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.27.2"
|
||||
$version="0.32.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.32"
|
||||
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 "Aug 2022" "fzf 0.32.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
@@ -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 "Aug 2022" "fzf 0.32.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -95,6 +95,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
.br
|
||||
.BR length " Prefers line with shorter length"
|
||||
.br
|
||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||
.br
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.br
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
@@ -135,10 +137,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 +301,12 @@ 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
|
||||
.TP
|
||||
.BI "--ellipsis=" "STR"
|
||||
Ellipsis to show when line is truncated (default: '..')
|
||||
.SS Display
|
||||
.TP
|
||||
.B "--ansi"
|
||||
@@ -440,7 +452,7 @@ e.g.
|
||||
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
|
||||
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
||||
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
|
||||
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR
|
||||
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
|
||||
@@ -460,7 +472,7 @@ e.g.
|
||||
done'\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"
|
||||
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -478,9 +490,9 @@ default until \fBtoggle-preview\fR action is triggered.
|
||||
execute the command in the background.
|
||||
|
||||
* Long lines are truncated by default. Line wrap can be enabled with
|
||||
\fB:wrap\fR flag.
|
||||
\fBwrap\fR flag.
|
||||
|
||||
* Preview window will automatically scroll to the bottom when \fB:follow\fR
|
||||
* Preview window will automatically scroll to the bottom when \fBfollow\fR
|
||||
flag is set, similarly to how \fBtail -f\fR works.
|
||||
|
||||
.RS
|
||||
@@ -492,7 +504,7 @@ e.g.
|
||||
done'\fR
|
||||
.RE
|
||||
|
||||
* Cyclic scrolling is enabled with \fB:cycle\fR flag.
|
||||
* Cyclic scrolling is enabled with \fBcycle\fR flag.
|
||||
|
||||
* To change the style of the border of the preview window, specify one of
|
||||
the options for \fB--border\fR with \fBborder-\fR prefix.
|
||||
@@ -542,6 +554,15 @@ e.g.
|
||||
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
|
||||
.RE
|
||||
|
||||
* You can specify an alternative set of options that are used only when the size
|
||||
of the preview window is below a certain threshold. Note that only one
|
||||
alternative layout is allowed.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fBfzf --preview 'cat {}' --preview-window 'right,border-left,<30(up,30%,border-bottom)'\fR
|
||||
.RE
|
||||
|
||||
.SS Scripting
|
||||
.TP
|
||||
.BI "-q, --query=" "STR"
|
||||
@@ -813,6 +834,8 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\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)
|
||||
@@ -853,7 +876,9 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBpreview-top\fR
|
||||
\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
|
||||
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
@@ -962,7 +987,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 +997,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)
|
||||
|
||||
|
||||
@@ -96,7 +96,12 @@ function! fzf#shellescape(arg, ...)
|
||||
if shell =~# 'cmd.exe$'
|
||||
return s:shellesc_cmd(a:arg)
|
||||
endif
|
||||
try
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
finally
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:fzf_getcwd()
|
||||
@@ -159,7 +164,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 = (&shell =~ 'powershell' ? '&' : '') . shellescape(a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
@@ -337,7 +342,8 @@ function! s:common_sink(action, lines) abort
|
||||
endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
|
||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -419,13 +425,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 +450,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 +483,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 +527,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 +672,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 +690,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 +889,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 +956,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 }"
|
||||
@@ -161,7 +161,11 @@ _fzf_handle_dynamic_completion() {
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
if [[ $cmd == \\* ]]; then
|
||||
cmd="${cmd:1}"
|
||||
fi
|
||||
cmd="${cmd//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
@@ -171,17 +175,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 +238,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")
|
||||
@@ -260,14 +264,6 @@ _fzf_dir_completion() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
local trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
COMP_WORDS[$COMP_CWORD]=$trigger
|
||||
elif [[ "$cur" != *"$trigger" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_fzf_proc_completion "$@"
|
||||
}
|
||||
|
||||
@@ -304,6 +300,10 @@ _fzf_alias_completion() {
|
||||
|
||||
# fzf options
|
||||
complete -o default -F _fzf_opts_completion fzf
|
||||
# fzf-tmux is a thin fzf wrapper that has only a few more options than fzf
|
||||
# itself. As a quick improvement we take fzf's completion. Adding the few extra
|
||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
@@ -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
|
||||
@@ -348,9 +348,6 @@ for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion (supports empty completion trigger)
|
||||
complete -F _fzf_complete_kill -o default -o bashdefault kill
|
||||
|
||||
unset cmd d_cmds a_cmds
|
||||
|
||||
_fzf_setup_completion() {
|
||||
@@ -373,9 +370,10 @@ _fzf_setup_completion() {
|
||||
done
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts
|
||||
# Environment variables / Aliases / Hosts / Process
|
||||
_fzf_setup_completion 'var' export unset
|
||||
_fzf_setup_completion 'alias' unalias
|
||||
_fzf_setup_completion 'host' ssh telnet
|
||||
_fzf_setup_completion 'proc' kill
|
||||
|
||||
fi
|
||||
|
||||
@@ -285,12 +285,6 @@ fzf-completion() {
|
||||
|
||||
lbuf=$LBUFFER
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
tail=$trigger
|
||||
tokens+=$trigger
|
||||
lbuf="$lbuf$trigger"
|
||||
fi
|
||||
|
||||
# Trigger sequence given
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
|
||||
@@ -14,45 +14,51 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
local cmd opts
|
||||
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS -m"
|
||||
eval "$cmd" |
|
||||
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
printf '%q ' "$item" # escape special chars
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
local selected="$(__fzf_select__)"
|
||||
local selected="$(__fzf_select__ "$@")"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd dir
|
||||
local cmd opts 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"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS +m"
|
||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
|
||||
}
|
||||
|
||||
__fzf_history__() {
|
||||
local output
|
||||
local output opts script
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0"
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' |
|
||||
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"
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS="$opts" $(__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 +72,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
|
||||
builtin cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
|
||||
@@ -66,7 +66,9 @@ fzf-file-widget() {
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey '^T' fzf-file-widget
|
||||
bindkey -M emacs '^T' fzf-file-widget
|
||||
bindkey -M vicmd '^T' fzf-file-widget
|
||||
bindkey -M viins '^T' fzf-file-widget
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
@@ -79,7 +81,7 @@ fzf-cd-widget() {
|
||||
return 0
|
||||
fi
|
||||
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||
BUFFER="cd ${(q)dir}"
|
||||
BUFFER="builtin cd -- ${(q)dir}"
|
||||
zle accept-line
|
||||
local ret=$?
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
@@ -87,13 +89,15 @@ fzf-cd-widget() {
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey '\ec' fzf-cd-widget
|
||||
bindkey -M emacs '\ec' fzf-cd-widget
|
||||
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||
bindkey -M viins '\ec' fzf-cd-widget
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
|
||||
selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
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 --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
@@ -106,7 +110,9 @@ fzf-history-widget() {
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
|
||||
} always {
|
||||
eval $__fzf_key_bindings_options
|
||||
|
||||
@@ -89,6 +89,9 @@ import (
|
||||
|
||||
var DEBUG bool
|
||||
|
||||
const delimiterChars = "/,:;|"
|
||||
const whiteChars = " \t\n\v\f\r\x85\xA0"
|
||||
|
||||
func indexAt(index int, max int, forward bool) int {
|
||||
if forward {
|
||||
return index
|
||||
@@ -107,7 +110,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
|
||||
@@ -117,6 +120,12 @@ const (
|
||||
// in web2 dictionary and my file system.
|
||||
bonusBoundary = scoreMatch / 2
|
||||
|
||||
// Extra bonus for word boundary after whitespace character or beginning of the string
|
||||
bonusBoundaryWhite = bonusBoundary + 2
|
||||
|
||||
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
|
||||
bonusBoundaryDelimiter = bonusBoundary + 1
|
||||
|
||||
// Although bonus point for non-word characters is non-contextual, we need it
|
||||
// for computing bonus points for consecutive chunks starting with a non-word
|
||||
// character.
|
||||
@@ -125,16 +134,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
|
||||
@@ -143,7 +152,9 @@ const (
|
||||
type charClass int
|
||||
|
||||
const (
|
||||
charNonWord charClass = iota
|
||||
charWhite charClass = iota
|
||||
charNonWord
|
||||
charDelimiter
|
||||
charLower
|
||||
charUpper
|
||||
charLetter
|
||||
@@ -181,6 +192,10 @@ func charClassOfAscii(char rune) charClass {
|
||||
return charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
return charNumber
|
||||
} else if strings.IndexRune(whiteChars, char) >= 0 {
|
||||
return charWhite
|
||||
} else if strings.IndexRune(delimiterChars, char) >= 0 {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
@@ -194,6 +209,10 @@ func charClassOfNonAscii(char rune) charClass {
|
||||
return charNumber
|
||||
} else if unicode.IsLetter(char) {
|
||||
return charLetter
|
||||
} else if unicode.IsSpace(char) {
|
||||
return charWhite
|
||||
} else if strings.IndexRune(delimiterChars, char) >= 0 {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
@@ -206,22 +225,33 @@ func charClassOf(char rune) charClass {
|
||||
}
|
||||
|
||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||
if prevClass == charNonWord && class != charNonWord {
|
||||
if class > charNonWord {
|
||||
if prevClass == charWhite {
|
||||
// Word boundary after whitespace
|
||||
return bonusBoundaryWhite
|
||||
} else if prevClass == charDelimiter {
|
||||
// Word boundary after a delimiter character
|
||||
return bonusBoundaryDelimiter
|
||||
} else if prevClass == charNonWord {
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
} else if prevClass == charLower && class == charUpper ||
|
||||
}
|
||||
}
|
||||
if prevClass == charLower && class == charUpper ||
|
||||
prevClass != charNumber && class == charNumber {
|
||||
// camelCase letter123
|
||||
return bonusCamel123
|
||||
} else if class == charNonWord {
|
||||
return bonusNonWord
|
||||
} else if class == charWhite {
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bonusAt(input *util.Chars, idx int) int16 {
|
||||
if idx == 0 {
|
||||
return bonusBoundary
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
}
|
||||
@@ -377,7 +407,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
// Phase 2. Calculate bonus for each point
|
||||
maxScore, maxScorePos := int16(0), 0
|
||||
pidx, lastIdx := 0, 0
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charWhite, false
|
||||
Tsub := T[idx:]
|
||||
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
||||
for off, char := range Tsub {
|
||||
@@ -417,14 +447,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
C0sub[off] = 1
|
||||
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, idx+off
|
||||
if forward && bonus == bonusBoundary {
|
||||
if forward && bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
}
|
||||
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 +507,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
|
||||
}
|
||||
@@ -486,11 +516,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
s1 = Hdiag[off] + scoreMatch
|
||||
b := Bsub[off]
|
||||
consecutive = Cdiag[off] + 1
|
||||
if consecutive > 1 {
|
||||
fb := B[col-int(consecutive)+1]
|
||||
// Break consecutive chunk
|
||||
if b == bonusBoundary {
|
||||
if b >= bonusBoundary && b > fb {
|
||||
consecutive = 1
|
||||
} else if consecutive > 1 {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
|
||||
} else {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
|
||||
}
|
||||
}
|
||||
if s1+b < s2 {
|
||||
s1 += Bsub[off]
|
||||
@@ -555,7 +588,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
||||
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
||||
pos := posArray(withPos, len(pattern))
|
||||
prevClass := charNonWord
|
||||
prevClass := charWhite
|
||||
if sidx > 0 {
|
||||
prevClass = charClassOf(text.Get(sidx - 1))
|
||||
}
|
||||
@@ -583,7 +616,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
// Break consecutive chunk
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||
@@ -598,7 +631,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
pidx++
|
||||
} else {
|
||||
if inGap {
|
||||
score += scoreGapExtention
|
||||
score += scoreGapExtension
|
||||
} else {
|
||||
score += scoreGapStart
|
||||
}
|
||||
@@ -741,7 +774,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
if bonus > bestBonus {
|
||||
bestPos, bestBonus = index, bonus
|
||||
}
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
index -= pidx - 1
|
||||
@@ -877,8 +910,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
|
||||
match = runesStr == string(pattern)
|
||||
}
|
||||
if match {
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundaryWhite)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundaryWhite}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
@@ -43,46 +43,46 @@ 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)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
|
||||
bonusBoundaryWhite*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,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+bonusBoundaryDelimiter*bonusFirstCharMultiplier+bonusBoundaryDelimiter*3)
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+bonusBoundaryDelimiter)
|
||||
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)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
|
||||
bonusBoundaryDelimiter*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+
|
||||
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)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite+
|
||||
scoreGapStart*2+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*3)
|
||||
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
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)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryDelimiter*2+
|
||||
scoreGapStart*2+scoreGapExtension*4)
|
||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtention*2)
|
||||
scoreMatch*3+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusCamel123*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))
|
||||
scoreMatch*4+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite*2+
|
||||
util.Max(bonusCamel123, bonusBoundaryWhite))
|
||||
|
||||
// Consecutive bonus updated
|
||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||
@@ -98,10 +98,10 @@ 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)
|
||||
scoreMatch*2+bonusBoundaryWhite*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||
scoreMatch*2+bonusBoundaryWhite*bonusFirstCharMultiplier+bonusBoundaryWhite)
|
||||
}
|
||||
|
||||
func TestExactMatchNaive(t *testing.T) {
|
||||
@@ -114,9 +114,9 @@ func TestExactMatchNaive(t *testing.T) {
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
|
||||
scoreMatch*4+bonusBoundaryDelimiter*(bonusFirstCharMultiplier+3))
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+bonusBoundaryDelimiter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrefixMatch(t *testing.T) {
|
||||
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
|
||||
score := scoreMatch*3 + bonusBoundaryWhite*bonusFirstCharMultiplier + bonusBoundaryWhite*2
|
||||
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||
@@ -156,9 +156,10 @@ func TestSuffixMatch(t *testing.T) {
|
||||
// Strip trailing white space from the string
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
|
||||
scoreMatch*3+bonusConsecutive*2)
|
||||
|
||||
// Only when the pattern doesn't end with a space
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
|
||||
scoreMatch*4+bonusConsecutive*2+bonusNonWord)
|
||||
scoreMatch*4+bonusConsecutive*2+bonusBoundaryWhite)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +183,9 @@ func TestNormalize(t *testing.T) {
|
||||
input, pattern, sidx, eidx, score)
|
||||
}
|
||||
}
|
||||
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
}
|
||||
|
||||
func TestLongString(t *testing.T) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
13
src/core.go
13
src/core.go
@@ -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)
|
||||
}
|
||||
@@ -241,9 +242,11 @@ func Run(opts *Options, version string, revision string) {
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func() []rune {
|
||||
input := func(reloaded bool) []rune {
|
||||
paused, input := terminal.Input()
|
||||
if !paused {
|
||||
if reloaded && paused {
|
||||
query = []rune{}
|
||||
} else if !paused {
|
||||
query = input
|
||||
}
|
||||
return query
|
||||
@@ -273,7 +276,8 @@ func Run(opts *Options, version string, revision string) {
|
||||
opts.Sync = false
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
|
||||
reset := clearCache()
|
||||
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
@@ -292,7 +296,8 @@ func Run(opts *Options, version string, revision string) {
|
||||
break
|
||||
}
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
|
||||
reset := clearCache()
|
||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
|
||||
165
src/options.go
165
src/options.go
@@ -35,7 +35,7 @@ const usage = `usage: fzf [options]
|
||||
--tac Reverse the order of the input
|
||||
--disabled Do not perform search
|
||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||
when the scores are tied [length|begin|end|index]
|
||||
when the scores are tied [length|chunk|begin|end|index]
|
||||
(default: length)
|
||||
|
||||
Interface
|
||||
@@ -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,8 @@ 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
|
||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
|
||||
|
||||
Display
|
||||
--ansi Enable processing of ANSI color codes
|
||||
@@ -85,7 +89,7 @@ const usage = `usage: fzf [options]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||
[,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
@@ -121,6 +125,7 @@ type criterion int
|
||||
|
||||
const (
|
||||
byScore criterion = iota
|
||||
byChunk
|
||||
byLength
|
||||
byBegin
|
||||
byEnd
|
||||
@@ -171,6 +176,18 @@ type previewOpts struct {
|
||||
follow bool
|
||||
border tui.BorderShape
|
||||
headerLines int
|
||||
threshold int
|
||||
alternative *previewOpts
|
||||
}
|
||||
|
||||
func (a previewOpts) sameLayout(b previewOpts) bool {
|
||||
return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden && a.threshold == b.threshold &&
|
||||
(a.alternative != nil && b.alternative != nil && a.alternative.sameLayout(*b.alternative) ||
|
||||
a.alternative == nil && b.alternative == nil)
|
||||
}
|
||||
|
||||
func (a previewOpts) sameContentLayout(b previewOpts) bool {
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
@@ -200,6 +217,7 @@ type Options struct {
|
||||
KeepRight bool
|
||||
Hscroll bool
|
||||
HscrollOff int
|
||||
ScrollOff int
|
||||
FileWord bool
|
||||
InfoStyle infoStyle
|
||||
JumpLabels string
|
||||
@@ -212,7 +230,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 +240,8 @@ type Options struct {
|
||||
History *History
|
||||
Header []string
|
||||
HeaderLines int
|
||||
HeaderFirst bool
|
||||
Ellipsis string
|
||||
Margin [4]sizeSpec
|
||||
Padding [4]sizeSpec
|
||||
BorderShape tui.BorderShape
|
||||
@@ -232,7 +252,7 @@ type Options struct {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -261,6 +281,7 @@ func defaultOptions() *Options {
|
||||
KeepRight: false,
|
||||
Hscroll: true,
|
||||
HscrollOff: 10,
|
||||
ScrollOff: 0,
|
||||
FileWord: false,
|
||||
InfoStyle: infoDefault,
|
||||
JumpLabels: defaultJumpLabels,
|
||||
@@ -273,7 +294,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 +304,8 @@ func defaultOptions() *Options {
|
||||
History: nil,
|
||||
Header: make([]string, 0),
|
||||
HeaderLines: 0,
|
||||
HeaderFirst: false,
|
||||
Ellipsis: "..",
|
||||
Margin: defaultMargin(),
|
||||
Padding: defaultMargin(),
|
||||
Unicode: true,
|
||||
@@ -589,6 +612,7 @@ func parseKeyChords(str string, message string) map[tui.Event]string {
|
||||
func parseTiebreak(str string) []criterion {
|
||||
criteria := []criterion{byScore}
|
||||
hasIndex := false
|
||||
hasChunk := false
|
||||
hasLength := false
|
||||
hasBegin := false
|
||||
hasEnd := false
|
||||
@@ -605,6 +629,9 @@ func parseTiebreak(str string) []criterion {
|
||||
switch str {
|
||||
case "index":
|
||||
check(&hasIndex, "index")
|
||||
case "chunk":
|
||||
check(&hasChunk, "chunk")
|
||||
criteria = append(criteria, byChunk)
|
||||
case "length":
|
||||
check(&hasLength, "length")
|
||||
criteria = append(criteria, byLength)
|
||||
@@ -618,6 +645,9 @@ func parseTiebreak(str string) []criterion {
|
||||
errorExit("invalid sort criterion: " + str)
|
||||
}
|
||||
}
|
||||
if len(criteria) > 4 {
|
||||
errorExit("at most 3 tiebreaks are allowed: " + str)
|
||||
}
|
||||
return criteria
|
||||
}
|
||||
|
||||
@@ -780,10 +810,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|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
|
||||
}
|
||||
|
||||
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,10 +822,16 @@ 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") {
|
||||
prefix = symbol + "unbind"
|
||||
} else if strings.HasPrefix(src[1:], "rebind") {
|
||||
prefix = symbol + "rebind"
|
||||
} else if strings.HasPrefix(src[1:], "change-prompt") {
|
||||
prefix = symbol + "change-prompt"
|
||||
} else if src[len(prefix)] == '-' {
|
||||
@@ -835,7 +871,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 +1010,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,10 +1031,16 @@ 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:
|
||||
offset = len("unbind")
|
||||
case actRebind:
|
||||
offset = len("rebind")
|
||||
case actExecuteSilent:
|
||||
offset = len("execute-silent")
|
||||
case actExecuteMulti:
|
||||
@@ -1004,17 +1052,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 || t == actRebind {
|
||||
parseKeyChords(actionArg, spec[0:offset]+" target required")
|
||||
} else if t == actChangePreviewWindow {
|
||||
opts := previewOpts{}
|
||||
for _, arg := range strings.Split(actionArg, "|") {
|
||||
parsePreviewWindow(&opts, arg)
|
||||
}
|
||||
if t == actUnbind {
|
||||
parseKeyChords(actionArg, "unbind target required")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1038,8 +1091,14 @@ func isExecuteAction(str string) actionType {
|
||||
return actReload
|
||||
case "unbind":
|
||||
return actUnbind
|
||||
case "rebind":
|
||||
return actRebind
|
||||
case "preview":
|
||||
return actPreview
|
||||
case "change-preview-window":
|
||||
return actChangePreviewWindow
|
||||
case "change-preview":
|
||||
return actChangePreview
|
||||
case "change-prompt":
|
||||
return actChangePrompt
|
||||
case "execute":
|
||||
@@ -1052,7 +1111,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")
|
||||
@@ -1122,12 +1181,19 @@ func parseInfoStyle(str string) infoStyle {
|
||||
}
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility
|
||||
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||
tokens := delimRegex.Split(input, -1)
|
||||
for _, token := range tokens {
|
||||
tokens := tokenRegex.FindAllStringSubmatch(input, -1)
|
||||
var alternative string
|
||||
for _, match := range tokens {
|
||||
if len(match[2]) > 0 {
|
||||
opts.threshold = atoi(match[2])
|
||||
alternative = match[3]
|
||||
continue
|
||||
}
|
||||
token := match[1]
|
||||
switch token {
|
||||
case "":
|
||||
case "default":
|
||||
@@ -1186,6 +1252,13 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(alternative) > 0 {
|
||||
alternativeOpts := *opts
|
||||
opts.alternative = &alternativeOpts
|
||||
opts.alternative.hidden = false
|
||||
opts.alternative.alternative = nil
|
||||
parsePreviewWindow(opts.alternative, alternative)
|
||||
}
|
||||
}
|
||||
|
||||
func parseMargin(opt string, margin string) [4]sizeSpec {
|
||||
@@ -1354,6 +1427,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 +1496,12 @@ 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 "--ellipsis":
|
||||
opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
|
||||
case "--preview":
|
||||
opts.Preview.command = nextString(allArgs, &i, "preview command required")
|
||||
case "--no-preview":
|
||||
@@ -1518,6 +1599,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Header = strLines(value)
|
||||
} else if match, value := optString(arg, "--header-lines="); match {
|
||||
opts.HeaderLines = atoi(value)
|
||||
} else if match, value := optString(arg, "--ellipsis="); match {
|
||||
opts.Ellipsis = value
|
||||
} else if match, value := optString(arg, "--preview="); match {
|
||||
opts.Preview.command = value
|
||||
} else if match, value := optString(arg, "--preview-window="); match {
|
||||
@@ -1530,6 +1613,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 +1632,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")
|
||||
}
|
||||
@@ -1580,11 +1669,6 @@ func validateSign(sign string, signOptName string) error {
|
||||
if sign == "" {
|
||||
return fmt.Errorf("%v cannot be empty", signOptName)
|
||||
}
|
||||
for _, r := range sign {
|
||||
if !unicode.IsGraphic(r) {
|
||||
return fmt.Errorf("invalid character in %v", signOptName)
|
||||
}
|
||||
}
|
||||
if runewidth.StringWidth(sign) > 2 {
|
||||
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
||||
}
|
||||
@@ -1592,7 +1676,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 +1692,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
|
||||
@@ -1648,10 +1750,25 @@ func postProcessOptions(opts *Options) {
|
||||
}
|
||||
}
|
||||
|
||||
func expectsArbitraryString(opt string) bool {
|
||||
switch opt {
|
||||
case "-q", "--query", "-f", "--filter", "--header", "--prompt":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseOptions parses command-line options
|
||||
func ParseOptions() *Options {
|
||||
opts := defaultOptions()
|
||||
|
||||
for idx, arg := range os.Args[1:] {
|
||||
if arg == "--version" && (idx == 0 || idx > 0 && !expectsArbitraryString(os.Args[idx])) {
|
||||
opts.Version = true
|
||||
return opts
|
||||
}
|
||||
}
|
||||
|
||||
// Options from Env var
|
||||
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
if len(words) > 0 {
|
||||
|
||||
@@ -65,6 +65,19 @@ func TestDelimiterRegexRegex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexRegexCaret(t *testing.T) {
|
||||
delim := delimiterRegexp(`(^\s*|\s+)`)
|
||||
tokens := Tokenize("foo bar baz", delim)
|
||||
if delim.str != nil ||
|
||||
len(tokens) != 4 ||
|
||||
tokens[0].text.ToString() != "" ||
|
||||
tokens[1].text.ToString() != "foo " ||
|
||||
tokens[2].text.ToString() != "bar " ||
|
||||
tokens[3].text.ToString() != "baz" {
|
||||
t.Errorf("%s %d", tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNth(t *testing.T) {
|
||||
{
|
||||
ranges := splitNth("..")
|
||||
@@ -440,8 +453,6 @@ func TestValidateSign(t *testing.T) {
|
||||
{"😀", true},
|
||||
{"", false},
|
||||
{">>>", false},
|
||||
{"\n", false},
|
||||
{"\t", false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !openbsd
|
||||
//go:build !openbsd
|
||||
|
||||
package protector
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build openbsd
|
||||
//go:build openbsd
|
||||
|
||||
package protector
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
case byScore:
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byChunk:
|
||||
b := minBegin
|
||||
e := maxEnd
|
||||
l := item.text.Length()
|
||||
for ; b >= 1; b-- {
|
||||
if unicode.IsSpace(item.text.Get(b - 1)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for ; e < l; e++ {
|
||||
if unicode.IsSpace(item.text.Get(e)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
val = util.AsUint16(e - b)
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byBegin, byEnd:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !386,!amd64
|
||||
//go:build !386 && !amd64
|
||||
|
||||
package fzf
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// +build !tcell
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
@@ -56,9 +54,9 @@ func TestResultRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
str := []rune("foo")
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
|
||||
if item1.points[3] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[2] != 3 || // Length
|
||||
item1.points[1] != 0 || // Unused
|
||||
@@ -67,7 +65,7 @@ func TestResultRank(t *testing.T) {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)
|
||||
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
@@ -100,6 +98,23 @@ func TestResultRank(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkTiebreak(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byChunk}
|
||||
|
||||
score := 100
|
||||
test := func(input string, offset Offset, chunk string) {
|
||||
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
|
||||
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
|
||||
t.Error(item.points)
|
||||
}
|
||||
}
|
||||
test("hello foobar goodbye", Offset{8, 9}, "foobar")
|
||||
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
|
||||
test("hello foobar goodbye", Offset{0, 1}, "hello")
|
||||
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build 386 amd64
|
||||
//go:build 386 || amd64
|
||||
|
||||
package fzf
|
||||
|
||||
|
||||
432
src/terminal.go
432
src/terminal.go
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
@@ -23,13 +24,32 @@ 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
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var activeTempFiles []string
|
||||
|
||||
const ellipsis string = ".."
|
||||
const clearCode string = "\x1b[2J"
|
||||
|
||||
func init() {
|
||||
@@ -101,6 +121,7 @@ type Terminal struct {
|
||||
keepRight bool
|
||||
hscroll bool
|
||||
hscrollOff int
|
||||
scrollOff int
|
||||
wordRubout string
|
||||
wordNext string
|
||||
cx int
|
||||
@@ -114,13 +135,17 @@ type Terminal struct {
|
||||
toggleSort bool
|
||||
delimiter Delimiter
|
||||
expect map[tui.Event]string
|
||||
keymap map[tui.Event][]action
|
||||
keymap map[tui.Event][]*action
|
||||
keymapOrg map[tui.Event][]*action
|
||||
pressed string
|
||||
printQuery bool
|
||||
history *History
|
||||
cycle bool
|
||||
headerFirst bool
|
||||
headerLines int
|
||||
header []string
|
||||
header0 []string
|
||||
ellipsis string
|
||||
ansi bool
|
||||
tabstop int
|
||||
margin [4]sizeSpec
|
||||
@@ -147,6 +172,7 @@ type Terminal struct {
|
||||
selected map[int32]selectedItem
|
||||
version int64
|
||||
reqBox *util.EventBox
|
||||
initialPreviewOpts previewOpts
|
||||
previewOpts previewOpts
|
||||
previewer previewer
|
||||
previewed previewed
|
||||
@@ -193,6 +219,7 @@ const (
|
||||
reqRefresh
|
||||
reqReinit
|
||||
reqRedraw
|
||||
reqFullRedraw
|
||||
reqClose
|
||||
reqPrintQuery
|
||||
reqPreviewEnqueue
|
||||
@@ -263,6 +290,8 @@ const (
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actPreview
|
||||
actChangePreview
|
||||
actChangePreviewWindow
|
||||
actPreviewTop
|
||||
actPreviewBottom
|
||||
actPreviewUp
|
||||
@@ -285,6 +314,7 @@ const (
|
||||
actSelect
|
||||
actDeselect
|
||||
actUnbind
|
||||
actRebind
|
||||
)
|
||||
|
||||
type placeholderFlags struct {
|
||||
@@ -303,6 +333,7 @@ type searchRequest struct {
|
||||
type previewRequest struct {
|
||||
template string
|
||||
pwindow tui.Window
|
||||
scrollOffset int
|
||||
list []*Item
|
||||
}
|
||||
|
||||
@@ -313,16 +344,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 +424,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 +456,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()
|
||||
}
|
||||
@@ -471,6 +503,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
|
||||
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
|
||||
}
|
||||
keymapCopy := make(map[tui.Event][]*action)
|
||||
for key, action := range opts.Keymap {
|
||||
keymapCopy[key] = action
|
||||
}
|
||||
t := Terminal{
|
||||
initDelay: delay,
|
||||
infoStyle: opts.InfoStyle,
|
||||
@@ -481,6 +517,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
keepRight: opts.KeepRight,
|
||||
hscroll: opts.Hscroll,
|
||||
hscrollOff: opts.HscrollOff,
|
||||
scrollOff: opts.ScrollOff,
|
||||
wordRubout: wordRubout,
|
||||
wordNext: wordNext,
|
||||
cx: len(input),
|
||||
@@ -495,6 +532,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
delimiter: opts.Delimiter,
|
||||
expect: opts.Expect,
|
||||
keymap: opts.Keymap,
|
||||
keymapOrg: keymapCopy,
|
||||
pressed: "",
|
||||
printQuery: opts.PrintQuery,
|
||||
history: opts.History,
|
||||
@@ -506,8 +544,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
paused: opts.Phony,
|
||||
strong: strongAttr,
|
||||
cycle: opts.Cycle,
|
||||
headerFirst: opts.HeaderFirst,
|
||||
headerLines: opts.HeaderLines,
|
||||
header: header,
|
||||
header0: header,
|
||||
ellipsis: opts.Ellipsis,
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
reading: true,
|
||||
@@ -520,8 +561,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
merger: EmptyMerger,
|
||||
selected: make(map[int32]selectedItem),
|
||||
reqBox: util.NewEventBox(),
|
||||
initialPreviewOpts: opts.Preview,
|
||||
previewOpts: opts.Preview,
|
||||
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
|
||||
previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, ""},
|
||||
previewed: previewed{0, 0, 0, false},
|
||||
previewBox: previewBox,
|
||||
eventBox: eventBox,
|
||||
@@ -638,6 +680,7 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||
t.merger = merger
|
||||
if reset {
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.version++
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
@@ -676,7 +719,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
|
||||
}
|
||||
|
||||
@@ -776,12 +819,15 @@ func (t *Terminal) resizeWindows() {
|
||||
}
|
||||
if t.window != nil {
|
||||
t.window.Close()
|
||||
t.window = nil
|
||||
}
|
||||
if t.pborder != nil {
|
||||
t.pborder.Close()
|
||||
t.pborder = nil
|
||||
}
|
||||
if t.pwindow != nil {
|
||||
t.pwindow.Close()
|
||||
t.pwindow = nil
|
||||
}
|
||||
// Reset preview version so that full redraw occurs
|
||||
t.previewed.version = 0
|
||||
@@ -826,19 +872,23 @@ func (t *Terminal) resizeWindows() {
|
||||
width = screenWidth - marginInt[1] - marginInt[3]
|
||||
height = screenHeight - marginInt[0] - marginInt[2]
|
||||
|
||||
// Set up preview window
|
||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||
if previewVisible {
|
||||
var resizePreviewWindows func(previewOpts previewOpts)
|
||||
resizePreviewWindows = func(previewOpts previewOpts) {
|
||||
hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil
|
||||
createPreviewWindow := func(y int, x int, w int, h int) {
|
||||
pwidth := w
|
||||
pheight := h
|
||||
var previewBorder tui.BorderStyle
|
||||
if t.previewOpts.border == tui.BorderNone {
|
||||
if previewOpts.border == tui.BorderNone {
|
||||
previewBorder = tui.MakeTransparentBorder()
|
||||
} else {
|
||||
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
|
||||
previewBorder = tui.MakeBorderStyle(previewOpts.border, t.unicode)
|
||||
}
|
||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
||||
switch t.previewOpts.border {
|
||||
switch previewOpts.border {
|
||||
case tui.BorderSharp, tui.BorderRounded:
|
||||
pwidth -= 4
|
||||
pheight -= 2
|
||||
@@ -865,7 +915,7 @@ func (t *Terminal) resizeWindows() {
|
||||
}
|
||||
verticalPad := 2
|
||||
minPreviewHeight := 3
|
||||
switch t.previewOpts.border {
|
||||
switch previewOpts.border {
|
||||
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
|
||||
verticalPad = 0
|
||||
minPreviewHeight = 1
|
||||
@@ -873,29 +923,46 @@ func (t *Terminal) resizeWindows() {
|
||||
verticalPad = 1
|
||||
minPreviewHeight = 2
|
||||
}
|
||||
switch t.previewOpts.position {
|
||||
case posUp:
|
||||
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
|
||||
switch previewOpts.position {
|
||||
case posUp, posDown:
|
||||
pheight := calculateSize(height, previewOpts.size, minHeight, minPreviewHeight, verticalPad)
|
||||
if hasThreshold && pheight < previewOpts.threshold {
|
||||
if !previewOpts.alternative.hidden {
|
||||
resizePreviewWindows(*previewOpts.alternative)
|
||||
}
|
||||
return
|
||||
}
|
||||
if previewOpts.position == posUp {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
|
||||
createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
|
||||
case posDown:
|
||||
pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad)
|
||||
} else {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
|
||||
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
|
||||
case posLeft:
|
||||
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
|
||||
}
|
||||
case posLeft, posRight:
|
||||
pwidth := calculateSize(width, previewOpts.size, minWidth, 5, 4)
|
||||
if hasThreshold && pwidth < previewOpts.threshold {
|
||||
if !previewOpts.alternative.hidden {
|
||||
resizePreviewWindows(*previewOpts.alternative)
|
||||
}
|
||||
return
|
||||
}
|
||||
if previewOpts.position == posLeft {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
|
||||
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
|
||||
case posRight:
|
||||
pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4)
|
||||
} else {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
|
||||
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
resizePreviewWindows(t.previewOpts)
|
||||
}
|
||||
if t.window == nil {
|
||||
t.window = t.tui.NewWindow(
|
||||
marginInt[0],
|
||||
marginInt[3],
|
||||
@@ -953,12 +1020,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 +1058,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 +1117,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 +1231,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
|
||||
}
|
||||
@@ -1206,47 +1294,54 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
|
||||
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
|
||||
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
|
||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
|
||||
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
|
||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
|
||||
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
|
||||
if displayWidth > maxWidth {
|
||||
transformOffsets := func(diff int32) {
|
||||
transformOffsets := func(diff int32, rightTrim bool) {
|
||||
for idx, offset := range offsets {
|
||||
b, e := offset.offset[0], offset.offset[1]
|
||||
b += 2 - diff
|
||||
e += 2 - diff
|
||||
b = util.Max32(b, 2)
|
||||
el := int32(len(ellipsis))
|
||||
b += el - diff
|
||||
e += el - diff
|
||||
b = util.Max32(b, el)
|
||||
if rightTrim {
|
||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||
}
|
||||
offsets[idx].offset[0] = b
|
||||
offsets[idx].offset[1] = util.Max32(b, e)
|
||||
}
|
||||
}
|
||||
if t.hscroll {
|
||||
if t.keepRight && pos == nil {
|
||||
trimmed, diff := t.trimLeft(text, maxWidth-2)
|
||||
transformOffsets(diff)
|
||||
text = append([]rune(ellipsis), trimmed...)
|
||||
} else if !t.overflow(text[:maxe], maxWidth-2) {
|
||||
trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
|
||||
transformOffsets(diff, false)
|
||||
text = append(ellipsis, trimmed...)
|
||||
} else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
|
||||
// Stri..
|
||||
text, _ = t.trimRight(text, maxWidth-2)
|
||||
text = append(text, []rune(ellipsis)...)
|
||||
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
|
||||
text = append(text, ellipsis...)
|
||||
} else {
|
||||
// Stri..
|
||||
if t.overflow(text[maxe:], 2) {
|
||||
text = append(text[:maxe], []rune(ellipsis)...)
|
||||
rightTrim := false
|
||||
if t.overflow(text[maxe:], ellipsisWidth) {
|
||||
text = append(text[:maxe], ellipsis...)
|
||||
rightTrim = true
|
||||
}
|
||||
// ..ri..
|
||||
var diff int32
|
||||
text, diff = t.trimLeft(text, maxWidth-2)
|
||||
text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
|
||||
|
||||
// Transform offsets
|
||||
transformOffsets(diff)
|
||||
text = append([]rune(ellipsis), text...)
|
||||
transformOffsets(diff, rightTrim)
|
||||
text = append(ellipsis, text...)
|
||||
}
|
||||
} else {
|
||||
text, _ = t.trimRight(text, maxWidth-2)
|
||||
text = append(text, []rune(ellipsis)...)
|
||||
text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
|
||||
text = append(text, ellipsis...)
|
||||
|
||||
for idx, offset := range offsets {
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
|
||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||
}
|
||||
}
|
||||
@@ -1346,6 +1441,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||
t.previewed.filled = true
|
||||
t.previewer.scrollable = true
|
||||
break
|
||||
} else if lineNo >= 0 {
|
||||
var fillRet tui.FillReturn
|
||||
@@ -1519,22 +1615,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 +1640,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
skipChars++
|
||||
case 'q':
|
||||
flags.query = true
|
||||
// query flag is not skipped
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -1609,9 +1690,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 +1708,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,50 +1734,41 @@ 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
|
||||
}
|
||||
|
||||
// Current query
|
||||
if match == "{q}" {
|
||||
case match == "{q}":
|
||||
return quoteEntry(query)
|
||||
}
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
items = selected
|
||||
}
|
||||
|
||||
replacements := make([]string, len(items))
|
||||
|
||||
if match == "{}" {
|
||||
for idx, item := range items {
|
||||
if flags.number {
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
case flags.number:
|
||||
n := int(item.text.Index)
|
||||
if n < 0 {
|
||||
replacements[idx] = ""
|
||||
} else {
|
||||
replacements[idx] = strconv.Itoa(n)
|
||||
return ""
|
||||
}
|
||||
} else if flags.file {
|
||||
replacements[idx] = item.AsString(stripAnsi)
|
||||
} else {
|
||||
replacements[idx] = quoteEntry(item.AsString(stripAnsi))
|
||||
return strconv.Itoa(n)
|
||||
case flags.file:
|
||||
return item.AsString(stripAnsi)
|
||||
default:
|
||||
return 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)
|
||||
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
|
||||
@@ -1698,26 +1776,44 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
ranges[idx] = r
|
||||
}
|
||||
|
||||
for idx, item := range items {
|
||||
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)
|
||||
}
|
||||
replacements[idx] = str
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
items = selected
|
||||
}
|
||||
replacements := make([]string, len(items))
|
||||
|
||||
for idx, item := range items {
|
||||
replacements[idx] = replace(item)
|
||||
}
|
||||
|
||||
if flags.file {
|
||||
return writeTemporaryFile(replacements, printsep)
|
||||
}
|
||||
@@ -1725,8 +1821,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Terminal) redraw() {
|
||||
func (t *Terminal) redraw(clear bool) {
|
||||
if clear {
|
||||
t.tui.Clear()
|
||||
}
|
||||
t.tui.Refresh()
|
||||
t.printAll()
|
||||
}
|
||||
@@ -1746,7 +1844,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 +1989,7 @@ func (t *Terminal) Loop() {
|
||||
go func() {
|
||||
for {
|
||||
<-resizeChan
|
||||
t.reqBox.Set(reqRedraw, nil)
|
||||
t.reqBox.Set(reqFullRedraw, nil)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -1930,12 +2028,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 +2047,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 +2184,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 +2239,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 +2311,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 +2356,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 +2369,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 +2387,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 +2549,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 +2580,7 @@ func (t *Terminal) Loop() {
|
||||
req(reqClose)
|
||||
}
|
||||
case actClearScreen:
|
||||
req(reqRedraw)
|
||||
req(reqFullRedraw)
|
||||
case actClearQuery:
|
||||
t.input = []rune{}
|
||||
t.cx = 0
|
||||
@@ -2627,7 +2729,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 +2759,59 @@ func (t *Terminal) Loop() {
|
||||
if valid {
|
||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||
newCommand = &command
|
||||
t.reading = true
|
||||
}
|
||||
case actUnbind:
|
||||
keys := parseKeyChords(a.a, "PANIC")
|
||||
for key := range keys {
|
||||
delete(t.keymap, key)
|
||||
}
|
||||
case actRebind:
|
||||
keys := parseKeyChords(a.a, "PANIC")
|
||||
for key := range keys {
|
||||
if originalAction, found := t.keymapOrg[key]; found {
|
||||
t.keymap[key] = originalAction
|
||||
}
|
||||
}
|
||||
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 +2819,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 +2881,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
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) + "'"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
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) + "'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
for len(text) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(text)
|
||||
if len(loc) < 2 {
|
||||
loc = []int{0, len(text)}
|
||||
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
||||
begin := 0
|
||||
for _, loc := range locs {
|
||||
tokens = append(tokens, text[begin:loc[1]])
|
||||
begin = loc[1]
|
||||
}
|
||||
last := util.Max(loc[1], 1)
|
||||
tokens = append(tokens, text[:last])
|
||||
text = text[last:]
|
||||
if begin < len(text) {
|
||||
tokens = append(tokens, text[begin:])
|
||||
}
|
||||
}
|
||||
return withPrefixLengths(tokens, 0)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// +build !ncurses
|
||||
// +build !tcell
|
||||
// +build !windows
|
||||
//go:build !tcell && !windows
|
||||
|
||||
package tui
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
defaultEscDelay = 100
|
||||
escPollInterval = 5
|
||||
offsetPollTries = 10
|
||||
maxInputBuffer = 10 * 1024
|
||||
maxInputBuffer = 1024 * 1024
|
||||
)
|
||||
|
||||
const consoleDevice string = "/dev/tty"
|
||||
@@ -60,7 +60,7 @@ func (r *LightRenderer) csi(code string) {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
fmt.Fprint(os.Stderr, r.queued.String())
|
||||
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,7 @@ func (r *LightRenderer) Init() {
|
||||
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
@@ -378,7 +379,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{Home, 0, nil}
|
||||
case 'F':
|
||||
return Event{End, 0, nil}
|
||||
case 'M':
|
||||
case '<':
|
||||
return r.mouseSequence(sz)
|
||||
case 'P':
|
||||
return Event{F1, 0, nil}
|
||||
@@ -519,19 +520,55 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
|
||||
func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
if len(r.buffer) < 6 || !r.mouse {
|
||||
// "\e[<0;0;0M"
|
||||
if len(r.buffer) < 9 || !r.mouse {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[3] {
|
||||
case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl
|
||||
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 36
|
||||
left := r.buffer[3] == 32
|
||||
down := r.buffer[3]%2 == 0
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
|
||||
rest := r.buffer[*sz:]
|
||||
end := bytes.IndexAny(rest, "mM")
|
||||
if end == -1 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
elems := strings.SplitN(string(rest[:end]), ";", 3)
|
||||
if len(elems) != 3 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
t := atoi(elems[0], -1)
|
||||
x := atoi(elems[1], -1) - 1
|
||||
y := atoi(elems[2], -1) - 1 - r.yoffset
|
||||
if t < 0 || x < 0 || y < 0 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz += end + 1
|
||||
|
||||
down := rest[end] == 'M'
|
||||
|
||||
scroll := 0
|
||||
if t >= 64 {
|
||||
t -= 64
|
||||
if t&0b1 == 1 {
|
||||
scroll = -1
|
||||
} else {
|
||||
scroll = 1
|
||||
}
|
||||
}
|
||||
|
||||
// middle := t & 0b1
|
||||
left := t&0b11 == 0
|
||||
|
||||
// shift := t & 0b100
|
||||
// ctrl := t & 0b1000
|
||||
mod := t&0b1100 > 0
|
||||
|
||||
if scroll != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
||||
}
|
||||
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
@@ -549,17 +586,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
|
||||
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 100
|
||||
s := 1 - int(r.buffer[3]%2)*2
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
@@ -597,6 +624,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
// It's highly likely that the offset we obtained at the beginning is
|
||||
// no longer correct, so we simply disable mouse input.
|
||||
r.csi("?1000l")
|
||||
r.csi("?1006l")
|
||||
r.mouse = false
|
||||
}
|
||||
}
|
||||
@@ -636,6 +664,7 @@ func (r *LightRenderer) Close() {
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
r.csi("?1006l")
|
||||
}
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package tui
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//+build windows
|
||||
//go:build windows
|
||||
|
||||
package tui
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build tcell windows
|
||||
//go:build tcell || windows
|
||||
|
||||
package tui
|
||||
|
||||
@@ -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
|
||||
_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:
|
||||
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 @@
|
||||
//go: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
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package tui
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package tui
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -34,6 +34,23 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
|
||||
return width, -1
|
||||
}
|
||||
|
||||
// Truncate returns the truncated runes and its width
|
||||
func Truncate(input string, limit int) ([]rune, int) {
|
||||
runes := []rune{}
|
||||
width := 0
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
w := runewidth.StringWidth(string(rs))
|
||||
if width+w > limit {
|
||||
return runes, width
|
||||
}
|
||||
width += w
|
||||
runes = append(runes, rs...)
|
||||
}
|
||||
return runes, width
|
||||
}
|
||||
|
||||
// Max returns the largest integer
|
||||
func Max(first int, second int) int {
|
||||
if first >= second {
|
||||
@@ -117,11 +134,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,29 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
truncated, width := Truncate("가나다라마", 7)
|
||||
if string(truncated) != "가나다" {
|
||||
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
|
||||
}
|
||||
if width != 6 {
|
||||
t.Errorf("Expected: 6, actual: %d", width)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package util
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package util
|
||||
|
||||
@@ -6,26 +6,60 @@ 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,
|
||||
CreationFlags: 0,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// KillCommand kills the process for the given command
|
||||
|
||||
192
test/test_go.rb
192
test/test_go.rb
@@ -754,6 +754,20 @@ class TestGoFZF < TestBase
|
||||
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_chunk
|
||||
writelines(tempname, [
|
||||
'1 foobarbaz baz',
|
||||
'2 foobar baz',
|
||||
'3 foo barbaz'
|
||||
])
|
||||
|
||||
assert_equal [
|
||||
'3 foo barbaz',
|
||||
'2 foobar baz',
|
||||
'1 foobarbaz baz'
|
||||
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_invalid_cache
|
||||
tmux.send_keys "(echo d; echo D; echo x) | #{fzf('-q d')}", :Enter
|
||||
tmux.until { |lines| assert_equal ' 2/3', lines[-2] }
|
||||
@@ -2043,8 +2057,8 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||
end
|
||||
|
||||
def test_unbind
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter
|
||||
def test_unbind_rebind
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d),e:rebind(c,d)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'ab'
|
||||
tmux.until { |lines| assert_equal '> ab', lines[-1] }
|
||||
@@ -2052,6 +2066,178 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys 'dabcd'
|
||||
tmux.until { |lines| assert_equal '> abcd', lines[-1] }
|
||||
tmux.send_keys 'ecabddc'
|
||||
tmux.until { |lines| assert_equal '> abdc', 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_reload_and_change_preview_should_update_preview
|
||||
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
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
|
||||
|
||||
def test_ellipsis
|
||||
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_match(/^> SNIPSNIP.*SNIPSNIP$/, lines[-3]) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2306,7 +2492,7 @@ module CompletionTest
|
||||
pid = lines[-1]&.split&.last
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.send_keys 'kill ', :Tab
|
||||
tmux.send_keys 'kill **', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'sleep12345'
|
||||
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
||||
|
||||
16
uninstall
16
uninstall
@@ -51,13 +51,8 @@ remove() {
|
||||
}
|
||||
|
||||
remove_line() {
|
||||
src=$(readlink "$1")
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Remove from $1 ($src):"
|
||||
else
|
||||
src=$1
|
||||
echo "Remove from $1:"
|
||||
fi
|
||||
|
||||
shift
|
||||
line_no=1
|
||||
@@ -75,8 +70,9 @@ remove_line() {
|
||||
echo " - Line #$line_no: $content"
|
||||
[ "$content" = "$1" ] || ask " - Remove?"
|
||||
if [ $? -eq 0 ]; then
|
||||
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" &&
|
||||
mv "$src.bak" "$src" || break
|
||||
temp=$(mktemp)
|
||||
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$temp" &&
|
||||
cat "$temp" > "$src" && rm -f "$temp" || break
|
||||
echo " - Removed"
|
||||
else
|
||||
echo " - Skipped"
|
||||
@@ -104,10 +100,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