mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-15 04:05:48 -07:00
Compare commits
152 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9e92b6f11e | ||
|
6cbde812f6 | ||
|
3b2e932c13 | ||
|
8ff4e52641 | ||
|
2dbc874e3d | ||
|
039a2f1d04 | ||
|
4ef1cf5b35 | ||
|
b44ab9e33c | ||
|
8f4c23f1c4 | ||
|
23a391e715 | ||
|
035b0be29f | ||
|
e1fcdbc337 | ||
|
cfc149e994 | ||
|
2faffbd1b7 | ||
|
8db65704b9 | ||
|
797a01aed4 | ||
|
bf515a3d32 | ||
|
a06745826a | ||
|
0420ed4f2a | ||
|
3b944addd4 | ||
|
70bf8bc35d | ||
|
724f8a1d45 | ||
|
cc2b2146ee | ||
|
8689f5f230 | ||
|
e9e0011f1d | ||
|
5b52833785 | ||
|
1525768094 | ||
|
a70ea4654e | ||
|
b02bf9b6bb | ||
|
bee7bc5324 | ||
|
7c2ffd3fef | ||
|
db01e7dab6 | ||
|
2326c74eb2 | ||
|
b9d15569e8 | ||
|
c3cc378d89 | ||
|
27d1f5e0a8 | ||
|
540632bb9e | ||
|
d9c028c934 | ||
|
c54ad82e8d | ||
|
295b89631b | ||
|
6179faf778 | ||
|
16dc236a25 | ||
|
61ae9d75b6 | ||
|
e2401aca68 | ||
|
59943cbb48 | ||
|
02634d404d | ||
|
ed12925f7d | ||
|
e0ddb97ab4 | ||
|
b8c01af0fc | ||
|
6de0a7ddc1 | ||
|
79196c025d | ||
|
94c33ac020 | ||
|
b2ecb6352c | ||
|
9dc3ed638a | ||
|
0acace1ace | ||
|
1a2d37e1e6 | ||
|
22adb6494f | ||
|
e023736c30 | ||
|
dca2262fe6 | ||
|
0684a20ea3 | ||
|
a1a72bb8d1 | ||
|
144d55a5be | ||
|
7fc13c5cfd | ||
|
dfee7af57b | ||
|
9b0e2daf02 | ||
|
590060a16b | ||
|
368294edf6 | ||
|
c4a9ccd6af | ||
|
cbf91f2ed3 | ||
|
b1460d4787 | ||
|
7dc9e14874 | ||
|
1616ed543d | ||
|
dc73fba188 | ||
|
ef148dfd37 | ||
|
93bbb3032d | ||
|
4c83d8596d | ||
|
d453e6d7db | ||
|
c29533994f | ||
|
1afe13b5b5 | ||
|
36600eaaa9 | ||
|
3ee1fc2034 | ||
|
e2f93e5a2d | ||
|
cfdf2f1153 | ||
|
e042143e3f | ||
|
7c613d0d9b | ||
|
b00d46bc14 | ||
|
555b0d235b | ||
|
564daf9a7d | ||
|
41bcbe342f | ||
|
dbe8dc344e | ||
|
e33fb59da1 | ||
|
7aa88aa115 | ||
|
2b6d600879 | ||
|
05c765d442 | ||
|
49b496269c | ||
|
7405925952 | ||
|
3afd543a7e | ||
|
b4f2cde5ac | ||
|
ed53ef7cee | ||
|
12630b124d | ||
|
1d59ac09d2 | ||
|
a8f3a0dd59 | ||
|
124cd70710 | ||
|
782de139c8 | ||
|
32eb32ee5e | ||
|
2f51eb2b41 | ||
|
0ccbd79e10 | ||
|
99bd6de541 | ||
|
1fef36e4bc | ||
|
89375005b5 | ||
|
88e78c9193 | ||
|
29a19ad080 | ||
|
2a039ab746 | ||
|
7e9a0fcdbd | ||
|
7a97532547 | ||
|
996abb2831 | ||
|
da500a358f | ||
|
c36b846acc | ||
|
d9b5c9b2be | ||
|
3dee8778d0 | ||
|
d4216b0dcc | ||
|
bfe2bf4dce | ||
|
561f9291fd | ||
|
b5b0d6b3ea | ||
|
a90426b7ca | ||
|
303c3bae7f | ||
|
6b4358f641 | ||
|
552158f3ad | ||
|
7205203dc8 | ||
|
0cadf70072 | ||
|
076b3d0a9a | ||
|
7b0c9e04d3 | ||
|
573df524fe | ||
|
aee417c46a | ||
|
04db44067d | ||
|
5b204c54f9 | ||
|
daa602422d | ||
|
04dfb14e32 | ||
|
c24256cba3 | ||
|
685fb71d89 | ||
|
83b6033906 | ||
|
01e7668915 | ||
|
0994d9c881 | ||
|
030428ba43 | ||
|
8a110e02b9 | ||
|
86d92c17c4 | ||
|
c4cc7891b4 | ||
|
218843b9f1 | ||
|
d274d093af | ||
|
6432f00f0d | ||
|
4e9e842aa4 | ||
|
07880ca441 |
3
.github/workflows/linux.yml
vendored
3
.github/workflows/linux.yml
vendored
@@ -46,6 +46,3 @@ jobs:
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
|
||||
- name: Integration test (tcell)
|
||||
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.21.0
|
||||
- uses: crate-ci/typos@v1.23.1
|
||||
|
@@ -109,12 +109,12 @@ release:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Tag }}'
|
||||
name_template: '{{ .Version }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-devel"
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
100
ADVANCED.md
100
ADVANCED.md
@@ -1,18 +1,17 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2024/01/20*
|
||||
* *Requires fzf 0.46.0 or above*
|
||||
* *Last update: 2024/06/24*
|
||||
* *Requires fzf 0.54.0 or later*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Screen Layout](#screen-layout)
|
||||
* [Display modes](#display-modes)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [`--tmux`](#--tmux)
|
||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
@@ -63,7 +62,7 @@ learn its wide variety of features.
|
||||
This document will guide you through some examples that will familiarize you
|
||||
with the advanced features of fzf.
|
||||
|
||||
Screen Layout
|
||||
Display modes
|
||||
-------------
|
||||
|
||||
### `--height`
|
||||
@@ -104,56 +103,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
|
||||
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||
```
|
||||
|
||||
### `fzf-tmux`
|
||||
### `--tmux`
|
||||
|
||||
Before fzf had `--height` option, we would open fzf in a tmux split pane not
|
||||
to take up the whole screen. This is done using `fzf-tmux` script.
|
||||
(Requires tmux 3.3 or later)
|
||||
|
||||
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
|
||||
|
||||
```sh
|
||||
# Open fzf on a tmux split pane below the current pane.
|
||||
# Takes the same set of options.
|
||||
fzf-tmux --layout=reverse
|
||||
# Open fzf in a tmux popup at the center of the screen with 70% width and height
|
||||
fzf --tmux 70%
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
|
||||
`--height` option. But the advantage of it is that it's more flexible.
|
||||
(See `man fzf-tmux` for available options.)
|
||||
`--tmux` option is silently ignored if you're not on tmux. So if you're trying
|
||||
to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
|
||||
|
||||
```sh
|
||||
# On the right (50%)
|
||||
fzf-tmux -r
|
||||
|
||||
# On the left (30%)
|
||||
fzf-tmux -l30%
|
||||
|
||||
# Above the cursor
|
||||
fzf-tmux -u30%
|
||||
# --tmux is specified later so it takes precedence over --height when on tmux.
|
||||
# If you're not on tmux, --tmux is ignored and --height is used instead.
|
||||
fzf --height 70% --tmux 70%
|
||||
```
|
||||
|
||||

|
||||
You can also specify the position, width, and height of the popup window in
|
||||
the following format:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### Popup window support
|
||||
|
||||
But here's the really cool part; tmux 3.2 added support for popup windows. So
|
||||
you can open fzf in a popup window, which is quite useful if you frequently
|
||||
use split panes.
|
||||
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
|
||||
|
||||
```sh
|
||||
# Open tmux in a tmux popup window (default size: 50% of the screen)
|
||||
fzf-tmux -p
|
||||
|
||||
# 80% width, 60% height
|
||||
fzf-tmux -p 80%,60%
|
||||
# 100% width and 60% height
|
||||
fzf --tmux 100%,60% --border horizontal
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```sh
|
||||
# On the right (50% width)
|
||||
fzf --tmux right
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# On the left (40% width and 70% height)
|
||||
fzf --tmux left,40%,70%
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!TIP]
|
||||
> You might also want to check out my tmux plugins which support this popup
|
||||
> window layout.
|
||||
>
|
||||
@@ -363,7 +361,7 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
@@ -374,11 +372,9 @@ INITIAL_QUERY="${*:-}"
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
|
||||
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
||||
process immediately via `start:reload` binding. This way, fzf owns the
|
||||
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
||||
the process will keep running in the background.
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
|
||||
the initial Ripgrep process immediately via `start:reload` binding for the
|
||||
consistency of the code.
|
||||
- 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
|
||||
@@ -402,7 +398,7 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
@@ -446,7 +442,7 @@ CTRL-F.
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
@@ -489,7 +485,7 @@ prevent immediate evaluation.
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||
@@ -529,15 +525,15 @@ Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
: | command='kubectl get pods --all-namespaces' fzf \
|
||||
command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start:reload:$command' \
|
||||
--bind 'ctrl-r:reload:$command' \
|
||||
--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 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
|
134
CHANGELOG.md
134
CHANGELOG.md
@@ -1,6 +1,140 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.54.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
|
||||
|
||||
- Implemented line wrap of long items
|
||||
- `--wrap` option enables line wrap
|
||||
- `--wrap-sign` customizes the sign for wrapped lines (default: `↳ `)
|
||||
- `toggle-wrap` action toggles line wrap
|
||||
```sh
|
||||
history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap' --wrap-sign $'\t↳ '
|
||||
```
|
||||
- fzf by default binds `CTRL-/` and `ALT-/` to `toggle-wrap`
|
||||
- Updated shell integration scripts to leverage line wrap
|
||||
- CTRL-R binding includes `--wrap-sign $'\t↳ '` to indent wrapped lines
|
||||
- `kill **` completion uses `--wrap` to show the whole line by default
|
||||
instead of showing it in the preview window
|
||||
- Added `--info-command` option for customizing the info line
|
||||
```sh
|
||||
# Prepend the current cursor position in yellow
|
||||
fzf --info-command='echo -e "\x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO 💛"'
|
||||
```
|
||||
- `$FZF_INFO` is set to the original info text
|
||||
- ANSI color codes are supported
|
||||
- Pointer and marker signs can be set to empty strings
|
||||
```sh
|
||||
# Minimal style
|
||||
fzf --pointer '' --marker '' --prompt '' --info hidden
|
||||
```
|
||||
- Better cache management and improved rendering for `--tail`
|
||||
- Improved `--sync` behavior
|
||||
- When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete.
|
||||
```sh
|
||||
# fzf will not render intermediate states
|
||||
(sleep 1; seq 1000000; sleep 1) |
|
||||
fzf --sync --query 5 --listen --bind start:up,load:up,result:up,focus:change-header:Ready
|
||||
```
|
||||
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
|
||||
```sh
|
||||
fzf --listen --sync --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .'
|
||||
```
|
||||
- Added `offset-middle` action to place the current item is in the middle of the screen
|
||||
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
|
||||
```sh
|
||||
# Now this will work as expected. Previously, this would print an invalid header line.
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `load` event would fire and the header would be prematurely updated.
|
||||
fzf --header 'Loading ...' --header-lines 1 \
|
||||
--bind 'start:reload:sleep 1; ps -ef' \
|
||||
--bind 'load:change-header:Loaded!'
|
||||
```
|
||||
- Fixed mouse support on Windows
|
||||
- Fixed crash when using `--tiebreak=end` with very long items
|
||||
- zsh 5.0 compatibility (thanks to @LangLangBart)
|
||||
- Fixed `--walker-skip` to also skip symlinks to directories
|
||||
- Fixed `result` event not fired when input stream is not complete
|
||||
- New tags will have `v` prefix so that they are available on https://proxy.golang.org/
|
||||
|
||||
0.53.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.53.0/_
|
||||
|
||||
- Multi-line display
|
||||
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
|
||||
- fzf can now display multi-line items
|
||||
```sh
|
||||
# All bash functions, highlighted
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi --highlight-line
|
||||
|
||||
# Ripgrep multi-line output
|
||||
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
|
||||
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
|
||||
```
|
||||
- To disable multi-line display, use `--no-multi-line`
|
||||
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
|
||||
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
|
||||
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
|
||||
```
|
||||
╻First line
|
||||
┃...
|
||||
╹Last line
|
||||
```
|
||||
- Native tmux integration
|
||||
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
|
||||
```sh
|
||||
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
# Center, 100% width and 70% height
|
||||
fzf --tmux 100%,70% --border horizontal --padding 1,2
|
||||
|
||||
# Left, 30% width
|
||||
fzf --tmux left,30%
|
||||
|
||||
# Bottom, 50% height
|
||||
fzf --tmux bottom,50%
|
||||
```
|
||||
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
|
||||
- To use `--tmux` in Vim plugin:
|
||||
```vim
|
||||
let g:fzf_layout = { 'tmux': '100%,70%' }
|
||||
```
|
||||
- Added support for endless input streams
|
||||
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
|
||||
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
|
||||
```sh
|
||||
# Interactive filtering of a log stream
|
||||
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
|
||||
```
|
||||
- Better Windows Support
|
||||
- fzf now works on Git bash (mintty) out of the box via winpty integration
|
||||
- Many fixes and improvements for Windows
|
||||
- man page is now embedded in the binary; `fzf --man` to see it
|
||||
- Changed the default `--scroll-off` to 3, as we think it's a better default
|
||||
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
|
||||
```sh
|
||||
# Vim will work fine without /dev/tty redirection
|
||||
ls | fzf --bind 'space:execute:vim {}' > selected
|
||||
```
|
||||
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
|
||||
```sh
|
||||
# This doesn't work as expected because --expect is not compatible with --bind
|
||||
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
|
||||
|
||||
# This is something you can do instead
|
||||
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
|
||||
```
|
||||
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
|
||||
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
|
||||
|
||||
0.52.1
|
||||
------
|
||||
- Fixed a critical bug in the Windows version
|
||||
- Windows users are strongly encouraged to upgrade to this version
|
||||
|
||||
0.52.0
|
||||
------
|
||||
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
|
||||
|
6
Makefile
6
Makefile
@@ -4,17 +4,17 @@ GOOS ?= $(shell $(GO) env GOOS)
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
|
||||
endif
|
||||
ifeq ($(VERSION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||
endif
|
||||
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
||||
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
|
||||
ifdef FZF_REVISION
|
||||
|
@@ -294,7 +294,7 @@ The following table summarizes the available options.
|
||||
| `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%`) |
|
||||
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
|
||||
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
|
||||
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
||||
|
||||
@@ -457,12 +457,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
```
|
||||
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` option value in `tmux` key.
|
||||
|
||||
```vim
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
|
@@ -57,7 +57,7 @@ if [[ $KITTY_WINDOW_ID ]]; then
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -f sixel -s "$dim" "$file"
|
||||
chafa -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
|
@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed. CTRL-Z is also disabled.
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
|
||||
# * --height option is not allowed
|
||||
# * CTRL-Z is also disabled
|
||||
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
|
@@ -311,7 +311,7 @@ The following table summarizes the available options.
|
||||
`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%` )
|
||||
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
|
||||
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
@@ -469,11 +469,12 @@ in Neovim.
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
<
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` options in `tmux` key.
|
||||
>
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
|
6
go.mod
6
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.3
|
||||
github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/term v0.20.0
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
12
go.sum
12
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
|
||||
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
||||
github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815 h1:4PRbYm9OMgH0bcdZZqMXA/AoOvpGy4l0H6g9Au/kgGA=
|
||||
github.com/charlievieth/fastwalk v1.0.7-0.20240703190418-87029d931815/go.mod h1:rV19+IF9Y2TYQNy4MqEk5M/spNHjKsA0i71yrsv2p4E=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
10
install
10
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.52.0
|
||||
version=0.54.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -146,7 +146,7 @@ download() {
|
||||
fi
|
||||
|
||||
local url
|
||||
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
||||
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -265,7 +265,11 @@ fi
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
if [[ "$shell" = zsh ]]; then
|
||||
echo "source <(fzf --$shell)" >> "$src"
|
||||
else
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
fi
|
||||
else
|
||||
cat >> "$src" << EOF
|
||||
# Auto-completion
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.52.0"
|
||||
$version="0.54.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
@@ -40,7 +40,7 @@ function download {
|
||||
return
|
||||
}
|
||||
cd "$fzf_base\bin"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
|
||||
$temp=$env:TMP + "\fzf.zip"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||
|
23
main.go
23
main.go
@@ -4,13 +4,14 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
fzf "github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.52"
|
||||
var version = "0.54"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
@@ -28,6 +29,9 @@ var zshCompletion []byte
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
//go:embed man/man1/fzf.1
|
||||
var manPage []byte
|
||||
|
||||
func printScript(label string, content []byte) {
|
||||
fmt.Println("### " + label + " ###")
|
||||
fmt.Println(strings.TrimSpace(string(content)))
|
||||
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
|
||||
}
|
||||
|
||||
func exit(code int, err error) {
|
||||
if err != nil {
|
||||
if code == fzf.ExitError && err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
os.Exit(code)
|
||||
@@ -76,6 +80,21 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
if options.Man {
|
||||
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
|
||||
if len(file) == 0 {
|
||||
fmt.Print(string(manPage))
|
||||
return
|
||||
}
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command("man", file)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Print(string(manPage))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
code, err := fzf.Run(options)
|
||||
exit(code, err)
|
||||
|
@@ -21,48 +21,48 @@ 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 "May 2024" "fzf 0.52.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Jul 2024" "fzf 0.54.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
.B fzf\-tmux [LAYOUT OPTIONS] [\-\-] [FZF OPTIONS]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
a tmux popup window. It is designed to work just like fzf except that it does
|
||||
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
|
||||
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
|
||||
your scripts as the extra options will be silently ignored if you're not on
|
||||
tmux.
|
||||
|
||||
.SH LAYOUT OPTIONS
|
||||
|
||||
(default layout: \fB-d 50%\fR)
|
||||
(default layout: \fB\-d 50%\fR)
|
||||
|
||||
.SS Popup window
|
||||
(requires tmux 3.2 or above)
|
||||
.TP
|
||||
.B "-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.TP
|
||||
.B "-w WIDTH[%]"
|
||||
.B "\-w WIDTH[%]"
|
||||
.TP
|
||||
.B "-h WIDTH[%]"
|
||||
.B "\-h WIDTH[%]"
|
||||
.TP
|
||||
.B "-x COL"
|
||||
.B "\-x COL"
|
||||
.TP
|
||||
.B "-y ROW"
|
||||
.B "\-y ROW"
|
||||
|
||||
.SS Split pane
|
||||
.TP
|
||||
.B "-u [height[%]]"
|
||||
.B "\-u [height[%]]"
|
||||
Split above (up)
|
||||
.TP
|
||||
.B "-d [height[%]]"
|
||||
.B "\-d [height[%]]"
|
||||
Split below (down)
|
||||
.TP
|
||||
.B "-l [width[%]]"
|
||||
.B "\-l [width[%]]"
|
||||
Split left
|
||||
.TP
|
||||
.B "-r [width[%]]"
|
||||
.B "\-r [width[%]]"
|
||||
Split right
|
||||
|
1038
man/man1/fzf.1
1038
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
@@ -95,7 +95,7 @@ function! s:shellesc_cmd(arg)
|
||||
let e .= c
|
||||
endfor
|
||||
let e .= repeat('\', slashes) .'"'
|
||||
return e
|
||||
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
@@ -327,7 +327,10 @@ function! s:common_sink(action, lines) abort
|
||||
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
||||
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
||||
for item in a:lines
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||
if has('win32unix') && item !~ '/'
|
||||
let item = substitute(item, '\', '/', 'g')
|
||||
end
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
|
||||
let sep = s:is_win ? '\' : '/'
|
||||
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
||||
endif
|
||||
@@ -487,6 +490,8 @@ function! s:extract_option(opts, name)
|
||||
return opt
|
||||
endfunction
|
||||
|
||||
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
@@ -529,18 +534,19 @@ try
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||
let use_term = has_nvim_term ||
|
||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_term = has_nvim_term || has_vim8_term
|
||||
\ && !s:need_cmd_window
|
||||
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_term
|
||||
let optstr .= ' --no-height'
|
||||
let optstr .= ' --no-height --no-tmux'
|
||||
elseif use_height
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
@@ -573,19 +579,21 @@ function! s:fzf_tmux(dict)
|
||||
if empty(size)
|
||||
for o in ['up', 'down', 'left', 'right']
|
||||
if s:present(a:dict, o)
|
||||
let spec = a:dict[o]
|
||||
if (o == 'up' || o == 'down') && spec[0] == '~'
|
||||
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
|
||||
else
|
||||
" Legacy boolean option
|
||||
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
|
||||
endif
|
||||
let size = o . ',' . a:dict[o]
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
|
||||
" Legacy fzf-tmux options
|
||||
if size =~ '-'
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
end
|
||||
|
||||
" Using native --tmux option
|
||||
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
|
||||
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
@@ -708,10 +716,10 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
call jobstart(cmd, fzf)
|
||||
return []
|
||||
endif
|
||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
||||
elseif s:need_cmd_window
|
||||
let shellscript = s:fzf_tempname()
|
||||
call s:writefile([command], shellscript)
|
||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
||||
let command = 'start //WAIT sh -c '.shellscript
|
||||
let a:temps.shellscript = shellscript
|
||||
endif
|
||||
if a:use_height
|
||||
|
@@ -101,75 +101,84 @@ _fzf_opts_completion() {
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-h --help
|
||||
-e --exact
|
||||
+x --no-extended
|
||||
-q --query
|
||||
-f --filter
|
||||
--literal
|
||||
--scheme
|
||||
--expect
|
||||
--disabled
|
||||
--tiebreak
|
||||
--bind
|
||||
--color
|
||||
-d --delimiter
|
||||
-n --nth
|
||||
--with-nth
|
||||
+s --no-sort
|
||||
--track
|
||||
--tac
|
||||
-i
|
||||
+i
|
||||
-m --multi
|
||||
--ansi
|
||||
--no-mouse
|
||||
+c --no-color
|
||||
--no-bold
|
||||
--layout
|
||||
--reverse
|
||||
--cycle
|
||||
--keep-right
|
||||
--no-hscroll
|
||||
--hscroll-off
|
||||
--scroll-off
|
||||
--filepath-word
|
||||
--info
|
||||
--separator
|
||||
--no-separator
|
||||
--no-scrollbar
|
||||
--jump-labels
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
--read0
|
||||
--print0
|
||||
--print-query
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--sync
|
||||
--history
|
||||
--history-size
|
||||
--header
|
||||
--header-lines
|
||||
--header-first
|
||||
--ellipsis
|
||||
--preview
|
||||
--preview-window
|
||||
--height
|
||||
--min-height
|
||||
+i --no-ignore-case
|
||||
+s --no-sort
|
||||
+x --no-extended
|
||||
--ansi
|
||||
--bash
|
||||
--bind
|
||||
--border
|
||||
--border-label
|
||||
--border-label-pos
|
||||
--color
|
||||
--cycle
|
||||
--disabled
|
||||
--ellipsis
|
||||
--expect
|
||||
--filepath-word
|
||||
--fish
|
||||
--header
|
||||
--header-first
|
||||
--header-lines
|
||||
--height
|
||||
--highlight-line
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--info
|
||||
--jump-labels
|
||||
--keep-right
|
||||
--layout
|
||||
--listen
|
||||
--listen-unsafe
|
||||
--literal
|
||||
--man
|
||||
--margin
|
||||
--marker
|
||||
--min-height
|
||||
--no-bold
|
||||
--no-clear
|
||||
--no-hscroll
|
||||
--no-mouse
|
||||
--no-scrollbar
|
||||
--no-separator
|
||||
--no-unicode
|
||||
--padding
|
||||
--pointer
|
||||
--preview
|
||||
--preview-label
|
||||
--preview-label-pos
|
||||
--no-unicode
|
||||
--margin
|
||||
--padding
|
||||
--preview-window
|
||||
--print-query
|
||||
--print0
|
||||
--prompt
|
||||
--read0
|
||||
--reverse
|
||||
--scheme
|
||||
--scroll-off
|
||||
--separator
|
||||
--sync
|
||||
--tabstop
|
||||
--listen
|
||||
--no-clear
|
||||
--tac
|
||||
--tiebreak
|
||||
--tmux
|
||||
--track
|
||||
--version
|
||||
--with-nth
|
||||
--with-shell
|
||||
--wrap
|
||||
--zsh
|
||||
-0 --exit-0
|
||||
-1 --select-1
|
||||
-d --delimiter
|
||||
-e --exact
|
||||
-f --filter
|
||||
-h --help
|
||||
-i --ignore-case
|
||||
-m --multi
|
||||
-n --nth
|
||||
-q --query
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
@@ -399,7 +408,7 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
@@ -476,7 +485,7 @@ d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||
# undocumented and subject to change in the future.
|
||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||
awk bat cat diff diff3
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
|
@@ -157,7 +157,8 @@ __fzf_generic_path_completion() {
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
export FZF_DEFAULT_OPTS
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
@@ -170,9 +171,9 @@ __fzf_generic_path_completion() {
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read item; do
|
||||
fi | while read -r item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n "${(q)item} "
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
@@ -197,11 +198,11 @@ _fzf_dir_completion() {
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
_fzf_feed_fifo() {
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
cat <&0 > "$1" &|
|
||||
}
|
||||
|
||||
_fzf_complete() {
|
||||
setopt localoptions ksh_arrays
|
||||
@@ -264,13 +265,14 @@ _fzf_complete_telnet() {
|
||||
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
||||
# The current word without the trigger is in the $prefix variable passed from the caller.
|
||||
_fzf_complete_ssh() {
|
||||
local tokens=(${(z)1})
|
||||
local -a tokens
|
||||
tokens=(${(z)1})
|
||||
case ${tokens[-1]} in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$prefix" "$1"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
@@ -296,7 +298,7 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
)
|
||||
|
@@ -57,15 +57,15 @@ __fzf_cd__() {
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output script
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
|
@@ -59,20 +59,31 @@ function fzf_key_bindings
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
|
||||
# merge history from other sessions before searching
|
||||
if test -z "$fish_private_mode"
|
||||
builtin history merge
|
||||
end
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
if type -P perl > /dev/null 2>&1
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
end
|
||||
else
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
end
|
||||
@@ -93,7 +104,7 @@ function fzf_key_bindings
|
||||
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd -- $result
|
||||
builtin cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
|
@@ -52,8 +52,8 @@ __fzf_select() {
|
||||
local item
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
@@ -106,16 +106,24 @@ fi
|
||||
|
||||
# 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 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
# Ensure the associative history array, which maps event numbers to the full
|
||||
# history lines, is loaded, and that Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
else
|
||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$(awk '{print $1}' <<< "$selected")
|
||||
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
|
||||
zle vi-fetch-history -n ${num%\*}
|
||||
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
zle vi-fetch-history -n $MATCH
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
|
@@ -58,72 +58,74 @@ func _() {
|
||||
_ = x[actToggleTrack-47]
|
||||
_ = x[actToggleTrackCurrent-48]
|
||||
_ = x[actToggleHeader-49]
|
||||
_ = x[actTrackCurrent-50]
|
||||
_ = x[actUntrackCurrent-51]
|
||||
_ = x[actDown-52]
|
||||
_ = x[actUp-53]
|
||||
_ = x[actPageUp-54]
|
||||
_ = x[actPageDown-55]
|
||||
_ = x[actPosition-56]
|
||||
_ = x[actHalfPageUp-57]
|
||||
_ = x[actHalfPageDown-58]
|
||||
_ = x[actOffsetUp-59]
|
||||
_ = x[actOffsetDown-60]
|
||||
_ = x[actJump-61]
|
||||
_ = x[actJumpAccept-62]
|
||||
_ = x[actPrintQuery-63]
|
||||
_ = x[actRefreshPreview-64]
|
||||
_ = x[actReplaceQuery-65]
|
||||
_ = x[actToggleSort-66]
|
||||
_ = x[actShowPreview-67]
|
||||
_ = x[actHidePreview-68]
|
||||
_ = x[actTogglePreview-69]
|
||||
_ = x[actTogglePreviewWrap-70]
|
||||
_ = x[actTransform-71]
|
||||
_ = x[actTransformBorderLabel-72]
|
||||
_ = x[actTransformHeader-73]
|
||||
_ = x[actTransformPreviewLabel-74]
|
||||
_ = x[actTransformPrompt-75]
|
||||
_ = x[actTransformQuery-76]
|
||||
_ = x[actPreview-77]
|
||||
_ = x[actChangePreview-78]
|
||||
_ = x[actChangePreviewWindow-79]
|
||||
_ = x[actPreviewTop-80]
|
||||
_ = x[actPreviewBottom-81]
|
||||
_ = x[actPreviewUp-82]
|
||||
_ = x[actPreviewDown-83]
|
||||
_ = x[actPreviewPageUp-84]
|
||||
_ = x[actPreviewPageDown-85]
|
||||
_ = x[actPreviewHalfPageUp-86]
|
||||
_ = x[actPreviewHalfPageDown-87]
|
||||
_ = x[actPrevHistory-88]
|
||||
_ = x[actPrevSelected-89]
|
||||
_ = x[actPut-90]
|
||||
_ = x[actNextHistory-91]
|
||||
_ = x[actNextSelected-92]
|
||||
_ = x[actExecute-93]
|
||||
_ = x[actExecuteSilent-94]
|
||||
_ = x[actExecuteMulti-95]
|
||||
_ = x[actSigStop-96]
|
||||
_ = x[actFirst-97]
|
||||
_ = x[actLast-98]
|
||||
_ = x[actReload-99]
|
||||
_ = x[actReloadSync-100]
|
||||
_ = x[actDisableSearch-101]
|
||||
_ = x[actEnableSearch-102]
|
||||
_ = x[actSelect-103]
|
||||
_ = x[actDeselect-104]
|
||||
_ = x[actUnbind-105]
|
||||
_ = x[actRebind-106]
|
||||
_ = x[actBecome-107]
|
||||
_ = x[actResponse-108]
|
||||
_ = x[actShowHeader-109]
|
||||
_ = x[actHideHeader-110]
|
||||
_ = x[actToggleWrap-50]
|
||||
_ = x[actTrackCurrent-51]
|
||||
_ = x[actUntrackCurrent-52]
|
||||
_ = x[actDown-53]
|
||||
_ = x[actUp-54]
|
||||
_ = x[actPageUp-55]
|
||||
_ = x[actPageDown-56]
|
||||
_ = x[actPosition-57]
|
||||
_ = x[actHalfPageUp-58]
|
||||
_ = x[actHalfPageDown-59]
|
||||
_ = x[actOffsetUp-60]
|
||||
_ = x[actOffsetDown-61]
|
||||
_ = x[actOffsetMiddle-62]
|
||||
_ = x[actJump-63]
|
||||
_ = x[actJumpAccept-64]
|
||||
_ = x[actPrintQuery-65]
|
||||
_ = x[actRefreshPreview-66]
|
||||
_ = x[actReplaceQuery-67]
|
||||
_ = x[actToggleSort-68]
|
||||
_ = x[actShowPreview-69]
|
||||
_ = x[actHidePreview-70]
|
||||
_ = x[actTogglePreview-71]
|
||||
_ = x[actTogglePreviewWrap-72]
|
||||
_ = x[actTransform-73]
|
||||
_ = x[actTransformBorderLabel-74]
|
||||
_ = x[actTransformHeader-75]
|
||||
_ = x[actTransformPreviewLabel-76]
|
||||
_ = x[actTransformPrompt-77]
|
||||
_ = x[actTransformQuery-78]
|
||||
_ = x[actPreview-79]
|
||||
_ = x[actChangePreview-80]
|
||||
_ = x[actChangePreviewWindow-81]
|
||||
_ = x[actPreviewTop-82]
|
||||
_ = x[actPreviewBottom-83]
|
||||
_ = x[actPreviewUp-84]
|
||||
_ = x[actPreviewDown-85]
|
||||
_ = x[actPreviewPageUp-86]
|
||||
_ = x[actPreviewPageDown-87]
|
||||
_ = x[actPreviewHalfPageUp-88]
|
||||
_ = x[actPreviewHalfPageDown-89]
|
||||
_ = x[actPrevHistory-90]
|
||||
_ = x[actPrevSelected-91]
|
||||
_ = x[actPrint-92]
|
||||
_ = x[actPut-93]
|
||||
_ = x[actNextHistory-94]
|
||||
_ = x[actNextSelected-95]
|
||||
_ = x[actExecute-96]
|
||||
_ = x[actExecuteSilent-97]
|
||||
_ = x[actExecuteMulti-98]
|
||||
_ = x[actSigStop-99]
|
||||
_ = x[actFirst-100]
|
||||
_ = x[actLast-101]
|
||||
_ = x[actReload-102]
|
||||
_ = x[actReloadSync-103]
|
||||
_ = x[actDisableSearch-104]
|
||||
_ = x[actEnableSearch-105]
|
||||
_ = x[actSelect-106]
|
||||
_ = x[actDeselect-107]
|
||||
_ = x[actUnbind-108]
|
||||
_ = x[actRebind-109]
|
||||
_ = x[actBecome-110]
|
||||
_ = x[actShowHeader-111]
|
||||
_ = x[actHideHeader-112]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
@@ -342,8 +342,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
state := interpretCode(code, prevState)
|
||||
if expected != state.ToString() {
|
||||
t.Errorf("expected: %s, actual: %s",
|
||||
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
|
||||
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
|
||||
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
|
||||
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
|
@@ -22,6 +22,14 @@ func (cc *ChunkCache) Clear() {
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) retire(chunk ...*Chunk) {
|
||||
cc.mutex.Lock()
|
||||
for _, c := range chunk {
|
||||
delete(cc.cache, c)
|
||||
}
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
|
@@ -16,14 +16,16 @@ type ChunkList struct {
|
||||
chunks []*Chunk
|
||||
mutex sync.Mutex
|
||||
trans ItemBuilder
|
||||
cache *ChunkCache
|
||||
}
|
||||
|
||||
// NewChunkList returns a new ChunkList
|
||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
|
||||
return &ChunkList{
|
||||
chunks: []*Chunk{},
|
||||
mutex: sync.Mutex{},
|
||||
trans: trans}
|
||||
trans: trans,
|
||||
cache: cache}
|
||||
}
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||
@@ -48,7 +50,12 @@ func CountItems(cs []*Chunk) int {
|
||||
if len(cs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
||||
if len(cs) == 1 {
|
||||
return cs[0].count
|
||||
}
|
||||
|
||||
// First chunk might not be full due to --tail=N
|
||||
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
|
||||
}
|
||||
|
||||
// Push adds the item to the list
|
||||
@@ -72,18 +79,56 @@ func (cl *ChunkList) Clear() {
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
changed := false
|
||||
if tail > 0 && CountItems(cl.chunks) > tail {
|
||||
changed = true
|
||||
// Find the number of chunks to keep
|
||||
numChunks := 0
|
||||
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
|
||||
numChunks++
|
||||
left -= cl.chunks[i].count
|
||||
}
|
||||
|
||||
// Copy the chunks to keep
|
||||
ret := make([]*Chunk, numChunks)
|
||||
minIndex := len(cl.chunks) - numChunks
|
||||
cl.cache.retire(cl.chunks[:minIndex]...)
|
||||
copy(ret, cl.chunks[minIndex:])
|
||||
|
||||
for left, i := tail, len(ret)-1; i >= 0; i-- {
|
||||
chunk := ret[i]
|
||||
if chunk.count > left {
|
||||
newChunk := *chunk
|
||||
newChunk.count = left
|
||||
oldCount := chunk.count
|
||||
for i := 0; i < left; i++ {
|
||||
newChunk.items[i] = chunk.items[oldCount-left+i]
|
||||
}
|
||||
ret[i] = &newChunk
|
||||
cl.cache.retire(chunk)
|
||||
break
|
||||
}
|
||||
left -= chunk.count
|
||||
}
|
||||
cl.chunks = ret
|
||||
}
|
||||
|
||||
ret := make([]*Chunk, len(cl.chunks))
|
||||
copy(ret, cl.chunks)
|
||||
|
||||
// Duplicate the last chunk
|
||||
// Duplicate the first and the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
if tail > 0 && cnt > 1 {
|
||||
newChunk := *ret[0]
|
||||
ret[0] = &newChunk
|
||||
}
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
|
||||
cl.mutex.Unlock()
|
||||
return ret, CountItems(ret)
|
||||
return ret, CountItems(ret), changed
|
||||
}
|
||||
|
@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
snapshot, count := cl.Snapshot()
|
||||
snapshot, count, _ := cl.Snapshot(0)
|
||||
if len(snapshot) > 0 || count > 0 {
|
||||
t.Error("Snapshot should be empty now")
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// But the new snapshot should contain the added items
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 1 && count != 2 {
|
||||
t.Error("Snapshot should not be empty now")
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// New snapshot
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||
t.Error("Expected two full chunks and one more chunk")
|
||||
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
|
||||
t.Error("Unexpected number of items:", lastChunkCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkListTail(t *testing.T) {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := 0; i < total; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
}
|
||||
|
||||
snapshot, count, changed := cl.Snapshot(0)
|
||||
assertCount := func(expected int, shouldChange bool) {
|
||||
if count != expected || CountItems(snapshot) != expected {
|
||||
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
|
||||
}
|
||||
if changed != shouldChange {
|
||||
t.Error("Unexpected change status")
|
||||
}
|
||||
}
|
||||
assertCount(total, false)
|
||||
|
||||
tail := chunkSize + chunkSize/2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, false)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(0)
|
||||
assertCount(tail, false)
|
||||
|
||||
tail = chunkSize / 2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
}
|
||||
|
@@ -58,6 +58,7 @@ const (
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
EvtReadFin
|
||||
EvtReadNone
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
@@ -67,9 +68,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
ExitCancel = -1
|
||||
ExitOk = 0
|
||||
ExitNoMatch = 1
|
||||
ExitError = 2
|
||||
ExitBecome = 126
|
||||
ExitInterrupt = 130
|
||||
)
|
||||
|
79
src/core.go
79
src/core.go
@@ -2,6 +2,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,8 +18,36 @@ Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
type revision struct {
|
||||
major int
|
||||
minor int
|
||||
}
|
||||
|
||||
func (r *revision) bumpMajor() {
|
||||
r.major++
|
||||
r.minor = 0
|
||||
}
|
||||
|
||||
func (r *revision) bumpMinor() {
|
||||
r.minor++
|
||||
}
|
||||
|
||||
func (r revision) compatible(other revision) bool {
|
||||
return r.major == other.major
|
||||
}
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options) (int, error) {
|
||||
if opts.Filter == nil {
|
||||
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
|
||||
if needWinpty(opts) {
|
||||
return runWinpty(os.Args, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if err := postProcessOptions(opts); err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
@@ -63,11 +92,12 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
@@ -79,7 +109,7 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
@@ -117,13 +147,20 @@ func Run(opts *Options) (int, error) {
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
|
||||
// Reader
|
||||
reloadOnStart := opts.reloadOnStart()
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
|
||||
if reloadOnStart {
|
||||
// reload or reload-sync action is bound to 'start' event, no need to start the reader
|
||||
eventBox.Set(EvtReadNone, nil)
|
||||
} else {
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
}
|
||||
}
|
||||
|
||||
// Matcher
|
||||
@@ -139,15 +176,14 @@ func Run(opts *Options) (int, error) {
|
||||
forward = true
|
||||
}
|
||||
}
|
||||
cache := NewChunkCache()
|
||||
patternCache := make(map[string]*Pattern)
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
inputRevision := 0
|
||||
snapshotRevision := 0
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
@@ -181,7 +217,8 @@ func Run(opts *Options) (int, error) {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
@@ -197,7 +234,8 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
if opts.Sync {
|
||||
sync := opts.Sync && !reloadOnStart
|
||||
if sync {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
}
|
||||
@@ -217,7 +255,7 @@ func Run(opts *Options) (int, error) {
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
deferred := opts.Select1 || opts.Exit0 || sync
|
||||
go terminal.Loop()
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
@@ -252,7 +290,7 @@ func Run(opts *Options) (int, error) {
|
||||
reading = true
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision++
|
||||
inputRevision.bumpMajor()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command, environ)
|
||||
}
|
||||
@@ -284,6 +322,9 @@ func Run(opts *Options) (int, error) {
|
||||
err = quitSignal.err
|
||||
stop = true
|
||||
return
|
||||
case EvtReadNone:
|
||||
reading = false
|
||||
terminal.UpdateCount(0, false, nil)
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand, nextEnviron)
|
||||
@@ -297,17 +338,18 @@ func Run(opts *Options) (int, error) {
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
if snapshotRevision != inputRevision {
|
||||
if !snapshotRevision.compatible(inputRevision) {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
var changed bool
|
||||
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
@@ -340,7 +382,10 @@ func Run(opts *Options) (int, error) {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
newSnapshot, newCount := chunkList.Snapshot()
|
||||
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || newCount > 0 {
|
||||
@@ -392,7 +437,7 @@ func Run(opts *Options) (int, error) {
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, true)
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func writeTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-preview-*")
|
||||
func WriteTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-temp-*")
|
||||
if err != nil {
|
||||
// Unable to create temporary file
|
||||
// FIXME: Should we terminate the program?
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var minItem = Item{text: util.Chars{Index: -1}}
|
||||
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
|
@@ -16,7 +16,7 @@ type MatchRequest struct {
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision int
|
||||
revision revision
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
@@ -30,7 +30,7 @@ type Matcher struct {
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
revision int
|
||||
revision revision
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
return &Matcher{
|
||||
cache: cache,
|
||||
@@ -82,11 +82,15 @@ func (m *Matcher) Loop() {
|
||||
break
|
||||
}
|
||||
|
||||
cacheCleared := false
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
m.cache.Clear()
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
@@ -95,20 +99,20 @@ func (m *Matcher) Loop() {
|
||||
cancelled := false
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
foundCache := false
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
foundCache = true
|
||||
merger = cached
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
|
||||
if !foundCache {
|
||||
if merger == nil {
|
||||
merger, cancelled = m.scan(request)
|
||||
}
|
||||
|
||||
@@ -160,6 +164,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -190,7 +195,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort {
|
||||
if m.sort && request.pattern.sortable {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
@@ -231,11 +236,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -244,7 +249,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
|
@@ -3,8 +3,8 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
func EmptyMerger(revision int) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision)
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
@@ -20,19 +20,25 @@ type Merger struct {
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision int
|
||||
revision revision
|
||||
minIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
}
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision}
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision}
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() int {
|
||||
func (mg *Merger) Revision() revision {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
index = int(itemIndex)
|
||||
index = int(itemIndex - mg.minIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
firstChunk := (*mg.chunks)[0]
|
||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||
idx -= firstChunk.count
|
||||
|
||||
chunk := (*mg.chunks)[idx/chunkSize+1]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
|
@@ -23,10 +23,11 @@ func randResult() Result {
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, EmptyMerger(0).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(0).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
|
||||
r := revision{}
|
||||
assert(t, EmptyMerger(r).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(r).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false, 0)
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false, 0)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false, 0)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
674
src/options.go
674
src/options.go
@@ -16,132 +16,153 @@ import (
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
const Usage = `usage: fzf [options]
|
||||
const Usage = `fzf is an interactive filter program for any kind of list.
|
||||
|
||||
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
|
||||
with omitted characters and still get the results you want.
|
||||
|
||||
Project URL: https://github.com/junegunn/fzf
|
||||
Author: Junegunn Choi <junegunn.c@gmail.com>
|
||||
|
||||
Usage: fzf [options]
|
||||
|
||||
Search
|
||||
-x, --extended Extended-search mode
|
||||
(enabled by default; +x or --no-extended to disable)
|
||||
-e, --exact Enable Exact-match
|
||||
-i Case-insensitive match (default: smart-case match)
|
||||
+i Case-sensitive match
|
||||
--scheme=SCHEME Scoring scheme [default|path|history]
|
||||
--literal Do not normalize latin script letters before matching
|
||||
-n, --nth=N[,..] Comma-separated list of field index expressions
|
||||
for limiting search scope. Each can be a non-zero
|
||||
integer or a range expression ([BEGIN]..[END]).
|
||||
--with-nth=N[,..] Transform the presentation of each line using
|
||||
field index expressions
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--track Track the current selection when the result is updated
|
||||
--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|chunk|begin|end|index]
|
||||
(default: length)
|
||||
|
||||
-x, --extended Extended-search mode
|
||||
(enabled by default; +x or --no-extended to disable)
|
||||
-e, --exact Enable Exact-match
|
||||
-i, --ignore-case Case-insensitive match (default: smart-case match)
|
||||
+i, --no-ignore-case Case-sensitive match
|
||||
--scheme=SCHEME Scoring scheme [default|path|history]
|
||||
--literal Do not normalize latin script letters before matching
|
||||
-n, --nth=N[,..] Comma-separated list of field index expressions
|
||||
for limiting search scope. Each can be a non-zero
|
||||
integer or a range expression ([BEGIN]..[END]).
|
||||
--with-nth=N[,..] Transform the presentation of each line using
|
||||
field index expressions
|
||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||
+s, --no-sort Do not sort the result
|
||||
--tail=NUM Maximum number of items to keep in memory
|
||||
--track Track the current selection when the result is updated
|
||||
--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|chunk|begin|end|index]
|
||||
(default: length)
|
||||
Interface
|
||||
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
||||
--no-mouse Disable mouse
|
||||
--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=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 mode
|
||||
-m, --multi[=MAX] Enable multi-select with tab/shift-tab
|
||||
--no-mouse Disable mouse
|
||||
--bind=KEYBINDS Custom key bindings. Refer to the man page.
|
||||
--cycle Enable cyclic scroll
|
||||
--wrap Enable line wrap
|
||||
--wrap-sign=STR Indicator for wrapped lines
|
||||
--no-multi-line Disable multi-line display of items when using --read0
|
||||
--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=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 mode
|
||||
|
||||
Layout
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calculated as the terminal height
|
||||
minus the given value.
|
||||
If prefixed with '~', fzf will determine the height
|
||||
according to the input size.
|
||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||
(default: 10)
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--border[=STYLE] Draw border around the finder
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--border-label=LABEL Label to print on the border
|
||||
--border-label-pos=COL Position of the border label
|
||||
[POSITIVE_INTEGER: columns from left|
|
||||
NEGATIVE_INTEGER: columns from right][:bottom]
|
||||
(default: 0 or center)
|
||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--info=STYLE Finder info style
|
||||
[default|right|hidden|inline[-right][:PREFIX]]
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||
--no-scrollbar Hide scrollbar
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--pointer=STR Pointer to the current line (default: '>')
|
||||
--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: '..')
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calculated as the terminal height
|
||||
minus the given value.
|
||||
If prefixed with '~', fzf will determine the height
|
||||
according to the input size.
|
||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||
(default: 10)
|
||||
--tmux[=OPTS] Start fzf in a tmux popup (requires tmux 3.3+)
|
||||
[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
(default: center,50%)
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--border[=STYLE] Draw border around the finder
|
||||
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--border-label=LABEL Label to print on the border
|
||||
--border-label-pos=COL Position of the border label
|
||||
[POSITIVE_INTEGER: columns from left|
|
||||
NEGATIVE_INTEGER: columns from right][:bottom]
|
||||
(default: 0 or center)
|
||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--info=STYLE Finder info style
|
||||
[default|right|hidden|inline[-right][:PREFIX]]
|
||||
--info-command=COMMAND Command to generate info line
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||
--no-scrollbar Hide scrollbar
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
--pointer=STR Pointer to the current line (default: '▌' or '>')
|
||||
--marker=STR Multi-select marker (default: '┃' or '>')
|
||||
--marker-multi-line=STR Multi-select marker for multi-line entries;
|
||||
3 elements for top, middle, and bottom (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
|
||||
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
||||
--highlight-line Highlight the whole current line
|
||||
--no-bold Do not use bold text
|
||||
--ansi Enable processing of ANSI color codes
|
||||
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
||||
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
|
||||
--highlight-line Highlight the whole current line
|
||||
--no-bold Do not use bold text
|
||||
|
||||
History
|
||||
--history=FILE History file
|
||||
--history-size=N Maximum number of history entries (default: 1000)
|
||||
--history=FILE History file
|
||||
--history-size=N Maximum number of history entries (default: 1000)
|
||||
|
||||
Preview
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][,SIZE[%]]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||
[,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][,SIZE[%]]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||
[,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
--preview-label=LABEL
|
||||
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
||||
but for preview window
|
||||
--preview-label-pos=N Same as --border-label and --border-label-pos,
|
||||
but for preview window
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
-1, --select-1 Automatically select the only match
|
||||
-0, --exit-0 Exit immediately when there's no match
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
--print-query Print query as the first line
|
||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--with-shell=STR Shell command and flags to start child processes with
|
||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||
(To allow remote process execution, use --listen-unsafe)
|
||||
--version Display version information and exit
|
||||
-q, --query=STR Start the finder with the given query
|
||||
-1, --select-1 Automatically select the only match
|
||||
-0, --exit-0 Exit immediately when there's no match
|
||||
-f, --filter=STR Filter mode. Do not start interactive finder.
|
||||
--print-query Print query as the first line
|
||||
--expect=KEYS Comma-separated list of keys to complete fzf
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--with-shell=STR Shell command and flags to start child processes with
|
||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||
(To allow remote process execution, use --listen-unsafe)
|
||||
|
||||
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
|
||||
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
||||
--walker-root=DIR Root directory from which to start walker (default: .)
|
||||
--walker-skip=DIRS Comma-separated list of directory names to skip
|
||||
(default: .git,node_modules)
|
||||
Directory traversal (Only used when $FZF_DEFAULT_COMMAND is not set)
|
||||
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
|
||||
--walker-root=DIR Root directory from which to start walker (default: .)
|
||||
--walker-skip=DIRS Comma-separated list of directory names to skip
|
||||
(default: .git,node_modules)
|
||||
|
||||
Shell integration
|
||||
--bash Print script to set up Bash shell integration
|
||||
--zsh Print script to set up Zsh shell integration
|
||||
--fish Print script to set up Fish shell integration
|
||||
--bash Print script to set up Bash shell integration
|
||||
--zsh Print script to set up Zsh shell integration
|
||||
--fish Print script to set up Fish shell integration
|
||||
|
||||
Help
|
||||
--version Display version information and exit
|
||||
--help Show this message
|
||||
--man Show man page
|
||||
|
||||
Environment variables
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
|
||||
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
|
||||
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
FZF_DEFAULT_OPTS Default options (e.g. '--layout=reverse --info=inline')
|
||||
FZF_DEFAULT_OPTS_FILE Location of the file to read default options from
|
||||
FZF_API_KEY X-API-Key header for HTTP server (--listen)
|
||||
|
||||
`
|
||||
|
||||
@@ -173,6 +194,7 @@ type heightSpec struct {
|
||||
percent bool
|
||||
auto bool
|
||||
inverse bool
|
||||
index int
|
||||
}
|
||||
|
||||
type sizeSpec struct {
|
||||
@@ -180,6 +202,13 @@ type sizeSpec struct {
|
||||
percent bool
|
||||
}
|
||||
|
||||
func (s sizeSpec) String() string {
|
||||
if s.percent {
|
||||
return fmt.Sprintf("%d%%", int(s.size))
|
||||
}
|
||||
return fmt.Sprintf("%d", int(s.size))
|
||||
}
|
||||
|
||||
func defaultMargin() [4]sizeSpec {
|
||||
return [4]sizeSpec{}
|
||||
}
|
||||
@@ -199,8 +228,16 @@ const (
|
||||
posDown
|
||||
posLeft
|
||||
posRight
|
||||
posCenter
|
||||
)
|
||||
|
||||
type tmuxOptions struct {
|
||||
width sizeSpec
|
||||
height sizeSpec
|
||||
position windowPosition
|
||||
index int
|
||||
}
|
||||
|
||||
type layoutType int
|
||||
|
||||
const (
|
||||
@@ -248,6 +285,77 @@ func (o *previewOpts) Toggle() {
|
||||
o.hidden = !o.hidden
|
||||
}
|
||||
|
||||
func defaultTmuxOptions(index int) *tmuxOptions {
|
||||
return &tmuxOptions{
|
||||
position: posCenter,
|
||||
width: sizeSpec{50, true},
|
||||
height: sizeSpec{50, true},
|
||||
index: index}
|
||||
}
|
||||
|
||||
func parseTmuxOptions(arg string, index int) (*tmuxOptions, error) {
|
||||
var err error
|
||||
opts := defaultTmuxOptions(index)
|
||||
tokens := splitRegexp.Split(arg, -1)
|
||||
errorToReturn := errors.New("invalid tmux option: " + arg + " (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])")
|
||||
if len(tokens) == 0 || len(tokens) > 3 {
|
||||
return nil, errorToReturn
|
||||
}
|
||||
|
||||
// Defaults to 'center'
|
||||
switch tokens[0] {
|
||||
case "top", "up":
|
||||
opts.position = posUp
|
||||
opts.width = sizeSpec{100, true}
|
||||
case "bottom", "down":
|
||||
opts.position = posDown
|
||||
opts.width = sizeSpec{100, true}
|
||||
case "left":
|
||||
opts.position = posLeft
|
||||
opts.height = sizeSpec{100, true}
|
||||
case "right":
|
||||
opts.position = posRight
|
||||
opts.height = sizeSpec{100, true}
|
||||
case "center":
|
||||
default:
|
||||
tokens = append([]string{"center"}, tokens...)
|
||||
}
|
||||
|
||||
// One size given
|
||||
var size1 sizeSpec
|
||||
if len(tokens) > 1 {
|
||||
if size1, err = parseSize(tokens[1], 100, "size"); err != nil {
|
||||
return nil, errorToReturn
|
||||
}
|
||||
}
|
||||
|
||||
// Two sizes given
|
||||
var size2 sizeSpec
|
||||
if len(tokens) == 3 {
|
||||
if size2, err = parseSize(tokens[2], 100, "size"); err != nil {
|
||||
return nil, errorToReturn
|
||||
}
|
||||
opts.width = size1
|
||||
opts.height = size2
|
||||
} else if len(tokens) == 2 {
|
||||
switch tokens[0] {
|
||||
case "top", "up":
|
||||
opts.height = size1
|
||||
case "bottom", "down":
|
||||
opts.height = size1
|
||||
case "left":
|
||||
opts.width = size1
|
||||
case "right":
|
||||
opts.width = size1
|
||||
case "center":
|
||||
opts.width = size1
|
||||
opts.height = size1
|
||||
}
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func parseLabelPosition(opts *labelOpts, arg string) error {
|
||||
opts.column = 0
|
||||
opts.bottom = false
|
||||
@@ -296,9 +404,14 @@ type walkerOpts struct {
|
||||
type Options struct {
|
||||
Input chan string
|
||||
Output chan string
|
||||
NoWinpty bool
|
||||
Tmux *tmuxOptions
|
||||
ForceTtyIn bool
|
||||
ProxyScript string
|
||||
Bash bool
|
||||
Zsh bool
|
||||
Fish bool
|
||||
Man bool
|
||||
Fuzzy bool
|
||||
FuzzyAlgo algo.Algo
|
||||
Scheme string
|
||||
@@ -312,6 +425,7 @@ type Options struct {
|
||||
Sort int
|
||||
Track trackOption
|
||||
Tac bool
|
||||
Tail int
|
||||
Criteria []criterion
|
||||
Multi int
|
||||
Ansi bool
|
||||
@@ -323,6 +437,9 @@ type Options struct {
|
||||
MinHeight int
|
||||
Layout layoutType
|
||||
Cycle bool
|
||||
Wrap bool
|
||||
WrapSign *string
|
||||
MultiLine bool
|
||||
CursorLine bool
|
||||
KeepRight bool
|
||||
Hscroll bool
|
||||
@@ -331,11 +448,13 @@ type Options struct {
|
||||
FileWord bool
|
||||
InfoStyle infoStyle
|
||||
InfoPrefix string
|
||||
InfoCommand string
|
||||
Separator *string
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
Pointer string
|
||||
Marker string
|
||||
Pointer *string
|
||||
Marker *string
|
||||
MarkerMulti *[3]string
|
||||
Query string
|
||||
Select1 bool
|
||||
Exit0 bool
|
||||
@@ -393,10 +512,18 @@ func defaultPreviewOpts(command string) previewOpts {
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
var theme *tui.ColorTheme
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
theme = tui.NoColorTheme()
|
||||
} else {
|
||||
theme = tui.EmptyTheme()
|
||||
}
|
||||
|
||||
return &Options{
|
||||
Bash: false,
|
||||
Zsh: false,
|
||||
Fish: false,
|
||||
Man: false,
|
||||
Fuzzy: true,
|
||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||
Scheme: "default",
|
||||
@@ -414,23 +541,26 @@ func defaultOptions() *Options {
|
||||
Multi: 0,
|
||||
Ansi: false,
|
||||
Mouse: true,
|
||||
Theme: tui.EmptyTheme(),
|
||||
Theme: theme,
|
||||
Black: false,
|
||||
Bold: true,
|
||||
MinHeight: 10,
|
||||
Layout: layoutDefault,
|
||||
Cycle: false,
|
||||
Wrap: false,
|
||||
MultiLine: true,
|
||||
KeepRight: false,
|
||||
Hscroll: true,
|
||||
HscrollOff: 10,
|
||||
ScrollOff: 0,
|
||||
ScrollOff: 3,
|
||||
FileWord: false,
|
||||
InfoStyle: infoDefault,
|
||||
Separator: nil,
|
||||
JumpLabels: defaultJumpLabels,
|
||||
Prompt: "> ",
|
||||
Pointer: ">",
|
||||
Marker: ">",
|
||||
Pointer: nil,
|
||||
Marker: nil,
|
||||
MarkerMulti: nil,
|
||||
Query: "",
|
||||
Select1: false,
|
||||
Exit0: false,
|
||||
@@ -554,7 +684,7 @@ func splitNth(str string) ([]Range, error) {
|
||||
|
||||
func delimiterRegexp(str string) Delimiter {
|
||||
// Special handling of \t
|
||||
str = strings.Replace(str, "\\t", "\t", -1)
|
||||
str = strings.ReplaceAll(str, "\\t", "\t")
|
||||
|
||||
// 1. Pattern does not contain any special character
|
||||
if regexp.QuoteMeta(str) == str {
|
||||
@@ -1077,7 +1207,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put|print)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1132,9 +1262,9 @@ Loop:
|
||||
masked += strings.Repeat(" ", loc[1])
|
||||
action = action[loc[1]:]
|
||||
}
|
||||
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
||||
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
||||
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
|
||||
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
|
||||
masked = strings.ReplaceAll(masked, ",:", string([]rune{escapedComma, ':'}))
|
||||
masked = strings.ReplaceAll(masked, "+:", string([]rune{escapedPlus, ':'}))
|
||||
return masked
|
||||
}
|
||||
|
||||
@@ -1241,6 +1371,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleTrackCurrent)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "toggle-wrap":
|
||||
appendAction(actToggleWrap)
|
||||
case "show-header":
|
||||
appendAction(actShowHeader)
|
||||
case "hide-header":
|
||||
@@ -1297,6 +1429,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actOffsetUp)
|
||||
case "offset-down":
|
||||
appendAction(actOffsetDown)
|
||||
case "offset-middle":
|
||||
appendAction(actOffsetMiddle)
|
||||
case "preview-top":
|
||||
appendAction(actPreviewTop)
|
||||
case "preview-bottom":
|
||||
@@ -1449,6 +1583,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actExecuteSilent
|
||||
case "execute-multi":
|
||||
return actExecuteMulti
|
||||
case "print":
|
||||
return actPrint
|
||||
case "put":
|
||||
return actPut
|
||||
case "transform":
|
||||
@@ -1516,8 +1652,8 @@ func parseSize(str string, maxPercent float64, label string) (sizeSpec, error) {
|
||||
return sizeSpec{val, percent}, nil
|
||||
}
|
||||
|
||||
func parseHeight(str string) (heightSpec, error) {
|
||||
heightSpec := heightSpec{}
|
||||
func parseHeight(str string, index int) (heightSpec, error) {
|
||||
heightSpec := heightSpec{index: index}
|
||||
if strings.HasPrefix(str, "~") {
|
||||
heightSpec.auto = true
|
||||
str = str[1:]
|
||||
@@ -1734,7 +1870,41 @@ func parseMargin(opt string, margin string) ([4]sizeSpec, error) {
|
||||
return [4]sizeSpec{}, errors.New("invalid " + opt + ": " + margin)
|
||||
}
|
||||
|
||||
func parseOptions(opts *Options, allArgs []string) error {
|
||||
func parseMarkerMultiLine(str string) (*[3]string, error) {
|
||||
if str == "" {
|
||||
return &[3]string{}, nil
|
||||
}
|
||||
gr := uniseg.NewGraphemes(str)
|
||||
parts := []string{}
|
||||
totalWidth := 0
|
||||
for gr.Next() {
|
||||
s := string(gr.Runes())
|
||||
totalWidth += uniseg.StringWidth(s)
|
||||
parts = append(parts, s)
|
||||
}
|
||||
|
||||
result := [3]string{}
|
||||
if totalWidth != 3 && totalWidth != 6 {
|
||||
return &result, fmt.Errorf("invalid total marker width: %d (expected: 0, 3 or 6)", totalWidth)
|
||||
}
|
||||
|
||||
expected := totalWidth / 3
|
||||
idx := 0
|
||||
for _, part := range parts {
|
||||
expected -= uniseg.StringWidth(part)
|
||||
result[idx] += part
|
||||
if expected <= 0 {
|
||||
idx++
|
||||
expected = totalWidth / 3
|
||||
}
|
||||
if idx == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
var err error
|
||||
var historyMax int
|
||||
if opts.History == nil {
|
||||
@@ -1768,10 +1938,16 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
opts.Fish = false
|
||||
opts.Help = false
|
||||
opts.Version = false
|
||||
opts.Man = false
|
||||
}
|
||||
startIndex := *index
|
||||
for i := 0; i < len(allArgs); i++ {
|
||||
arg := allArgs[i]
|
||||
index := i + startIndex
|
||||
switch arg {
|
||||
case "--man":
|
||||
clearExitingOpts()
|
||||
opts.Man = true
|
||||
case "--bash":
|
||||
clearExitingOpts()
|
||||
opts.Bash = true
|
||||
@@ -1787,6 +1963,29 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
case "--version":
|
||||
clearExitingOpts()
|
||||
opts.Version = true
|
||||
case "--no-winpty":
|
||||
opts.NoWinpty = true
|
||||
case "--tmux":
|
||||
given, str := optionalNextString(allArgs, &i)
|
||||
if given {
|
||||
if opts.Tmux, err = parseTmuxOptions(str, index); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
opts.Tmux = defaultTmuxOptions(index)
|
||||
}
|
||||
case "--no-tmux":
|
||||
opts.Tmux = nil
|
||||
case "--force-tty-in":
|
||||
// NOTE: We need this because `system('fzf --tmux < /dev/tty')` doesn't
|
||||
// work on Neovim. Same as '-' option of fzf-tmux.
|
||||
opts.ForceTtyIn = true
|
||||
case "--no-force-tty-in":
|
||||
opts.ForceTtyIn = false
|
||||
case "--proxy-script":
|
||||
if opts.ProxyScript, err = nextString(allArgs, &i, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
case "-x", "--extended":
|
||||
opts.Extended = true
|
||||
case "-e", "--exact":
|
||||
@@ -1914,9 +2113,18 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
opts.Tac = true
|
||||
case "--no-tac":
|
||||
opts.Tac = false
|
||||
case "-i":
|
||||
case "--tail":
|
||||
if opts.Tail, err = nextInt(allArgs, &i, "number of items to keep required"); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Tail <= 0 {
|
||||
return errors.New("number of items to keep must be a positive integer")
|
||||
}
|
||||
case "--no-tail":
|
||||
opts.Tail = 0
|
||||
case "-i", "--ignore-case":
|
||||
opts.Case = CaseIgnore
|
||||
case "+i":
|
||||
case "+i", "--no-ignore-case":
|
||||
opts.Case = CaseRespect
|
||||
case "-m", "--multi":
|
||||
if opts.Multi, err = optionalNumeric(allArgs, &i, maxMulti); err != nil {
|
||||
@@ -1962,6 +2170,20 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
opts.CursorLine = false
|
||||
case "--no-cycle":
|
||||
opts.Cycle = false
|
||||
case "--wrap":
|
||||
opts.Wrap = true
|
||||
case "--no-wrap":
|
||||
opts.Wrap = false
|
||||
case "--wrap-sign":
|
||||
str, err := nextString(allArgs, &i, "wrap sign required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.WrapSign = &str
|
||||
case "--multi-line":
|
||||
opts.MultiLine = true
|
||||
case "--no-multi-line":
|
||||
opts.MultiLine = false
|
||||
case "--keep-right":
|
||||
opts.KeepRight = true
|
||||
case "--no-keep-right":
|
||||
@@ -1990,6 +2212,12 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--info-command":
|
||||
if opts.InfoCommand, err = nextString(allArgs, &i, "info command required"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-info-command":
|
||||
opts.InfoCommand = ""
|
||||
case "--no-info":
|
||||
opts.InfoStyle = infoHidden
|
||||
case "--inline-info":
|
||||
@@ -2049,17 +2277,27 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
return err
|
||||
}
|
||||
case "--pointer":
|
||||
str, err := nextString(allArgs, &i, "pointer sign string required")
|
||||
str, err := nextString(allArgs, &i, "pointer sign required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Pointer = firstLine(str)
|
||||
str = firstLine(str)
|
||||
opts.Pointer = &str
|
||||
case "--marker":
|
||||
str, err := nextString(allArgs, &i, "selected sign string required")
|
||||
str, err := nextString(allArgs, &i, "marker sign required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Marker = firstLine(str)
|
||||
str = firstLine(str)
|
||||
opts.Marker = &str
|
||||
case "--marker-multi-line":
|
||||
str, err := nextString(allArgs, &i, "marker sign for multi-line entries required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(str)); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--sync":
|
||||
opts.Sync = true
|
||||
case "--no-sync", "--async":
|
||||
@@ -2123,7 +2361,7 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Height, err = parseHeight(str); err != nil {
|
||||
if opts.Height, err = parseHeight(str, index); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--min-height":
|
||||
@@ -2264,6 +2502,10 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
if opts.FuzzyAlgo, err = parseAlgo(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--tmux="); match {
|
||||
if opts.Tmux, err = parseTmuxOptions(value, index); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--scheme="); match {
|
||||
opts.Scheme = strings.ToLower(value)
|
||||
} else if match, value := optString(arg, "-q", "--query="); match {
|
||||
@@ -2288,12 +2530,20 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
if err := parseLabelPosition(&opts.PreviewLabel, value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--wrap-sign="); match {
|
||||
opts.WrapSign = &value
|
||||
} else if match, value := optString(arg, "--prompt="); match {
|
||||
opts.Prompt = value
|
||||
} else if match, value := optString(arg, "--pointer="); match {
|
||||
opts.Pointer = firstLine(value)
|
||||
str := firstLine(value)
|
||||
opts.Pointer = &str
|
||||
} else if match, value := optString(arg, "--marker="); match {
|
||||
opts.Marker = firstLine(value)
|
||||
str := firstLine(value)
|
||||
opts.Marker = &str
|
||||
} else if match, value := optString(arg, "--marker-multi-line="); match {
|
||||
if opts.MarkerMulti, err = parseMarkerMultiLine(firstLine(value)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "-n", "--nth="); match {
|
||||
if opts.Nth, err = splitNth(value); err != nil {
|
||||
return err
|
||||
@@ -2309,7 +2559,7 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--height="); match {
|
||||
if opts.Height, err = parseHeight(value); err != nil {
|
||||
if opts.Height, err = parseHeight(value, index); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--min-height="); match {
|
||||
@@ -2324,6 +2574,8 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--info-command="); match {
|
||||
opts.InfoCommand = value
|
||||
} else if match, value := optString(arg, "--separator="); match {
|
||||
opts.Separator = &value
|
||||
} else if match, value := optString(arg, "--scrollbar="); match {
|
||||
@@ -2425,11 +2677,19 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
} else if match, value := optString(arg, "--jump-labels="); match {
|
||||
opts.JumpLabels = value
|
||||
validateJumpLabels = true
|
||||
} else if match, value := optString(arg, "--tail="); match {
|
||||
if opts.Tail, err = atoi(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.Tail <= 0 {
|
||||
return errors.New("number of items to keep must be a positive integer")
|
||||
}
|
||||
} else {
|
||||
return errors.New("unknown option: " + arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
*index += len(allArgs)
|
||||
|
||||
if opts.HeaderLines < 0 {
|
||||
return errors.New("header lines must be a non-negative integer")
|
||||
@@ -2462,32 +2722,23 @@ func parseOptions(opts *Options, allArgs []string) error {
|
||||
}
|
||||
|
||||
func validateSign(sign string, signOptName string) error {
|
||||
if sign == "" {
|
||||
return fmt.Errorf("%v cannot be empty", signOptName)
|
||||
}
|
||||
if uniseg.StringWidth(sign) > 2 {
|
||||
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function can have side-effects and alter some global states.
|
||||
// So we run it on fzf.Run and not on ParseOptions.
|
||||
func postProcessOptions(opts *Options) error {
|
||||
if opts.Ambidouble {
|
||||
uniseg.EastAsianAmbiguousWidth = 2
|
||||
func validateOptions(opts *Options) error {
|
||||
if opts.Pointer != nil {
|
||||
if err := validateSign(*opts.Pointer, "pointer"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateSign(opts.Pointer, "pointer"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateSign(opts.Marker, "marker"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
return errors.New("--height option is currently not supported on this platform")
|
||||
if opts.Marker != nil {
|
||||
if err := validateSign(*opts.Marker, "marker"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Scrollbar != nil {
|
||||
@@ -2502,6 +2753,82 @@ func postProcessOptions(opts *Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Height.auto {
|
||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
||||
if s.percent {
|
||||
return errors.New("adaptive height is not compatible with top/bottom percent margin")
|
||||
}
|
||||
}
|
||||
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
|
||||
if s.percent {
|
||||
return errors.New("adaptive height is not compatible with top/bottom percent padding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function can have side-effects and alter some global states.
|
||||
// So we run it on fzf.Run and not on ParseOptions.
|
||||
func postProcessOptions(opts *Options) error {
|
||||
if opts.Ambidouble {
|
||||
uniseg.EastAsianAmbiguousWidth = 2
|
||||
}
|
||||
|
||||
if opts.BorderShape == tui.BorderUndefined {
|
||||
opts.BorderShape = tui.BorderNone
|
||||
}
|
||||
|
||||
if opts.Pointer == nil {
|
||||
defaultPointer := "▌"
|
||||
if !opts.Unicode {
|
||||
defaultPointer = ">"
|
||||
}
|
||||
opts.Pointer = &defaultPointer
|
||||
}
|
||||
|
||||
markerLen := 1
|
||||
if opts.Marker == nil {
|
||||
if opts.MarkerMulti != nil && opts.MarkerMulti[0] == "" {
|
||||
empty := ""
|
||||
opts.Marker = &empty
|
||||
markerLen = 0
|
||||
} else {
|
||||
// "▎" looks better, but not all terminals render it correctly
|
||||
defaultMarker := "┃"
|
||||
if !opts.Unicode {
|
||||
defaultMarker = ">"
|
||||
}
|
||||
opts.Marker = &defaultMarker
|
||||
}
|
||||
} else {
|
||||
markerLen = uniseg.StringWidth(*opts.Marker)
|
||||
}
|
||||
|
||||
markerMultiLen := 1
|
||||
if opts.MarkerMulti == nil {
|
||||
if *opts.Marker == "" {
|
||||
opts.MarkerMulti = &[3]string{}
|
||||
markerMultiLen = 0
|
||||
} else if opts.Unicode {
|
||||
opts.MarkerMulti = &[3]string{"╻", "┃", "╹"}
|
||||
} else {
|
||||
opts.MarkerMulti = &[3]string{".", "|", "'"}
|
||||
}
|
||||
} else {
|
||||
markerMultiLen = uniseg.StringWidth(opts.MarkerMulti[0])
|
||||
}
|
||||
diff := markerMultiLen - markerLen
|
||||
if diff > 0 {
|
||||
padded := *opts.Marker + strings.Repeat(" ", diff)
|
||||
opts.Marker = &padded
|
||||
} else if diff < 0 {
|
||||
for idx := range opts.MarkerMulti {
|
||||
opts.MarkerMulti[idx] += strings.Repeat(" ", -diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
if opts.History != nil {
|
||||
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
|
||||
@@ -2548,19 +2875,6 @@ func postProcessOptions(opts *Options) error {
|
||||
opts.Keymap[tui.DoubleClick.AsEvent()] = opts.Keymap[tui.CtrlM.AsEvent()]
|
||||
}
|
||||
|
||||
if opts.Height.auto {
|
||||
for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
|
||||
if s.percent {
|
||||
return errors.New("adaptive height is not compatible with top/bottom percent margin")
|
||||
}
|
||||
}
|
||||
for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
|
||||
if s.percent {
|
||||
return errors.New("adaptive height is not compatible with top/bottom percent padding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not using extended search mode, --nth option becomes irrelevant
|
||||
// if it contains the whole range
|
||||
if !opts.Extended || len(opts.Nth) == 1 {
|
||||
@@ -2589,12 +2903,22 @@ func postProcessOptions(opts *Options) error {
|
||||
theme.Spinner = boldify(theme.Spinner)
|
||||
}
|
||||
|
||||
// If --height option is not supported on the platform, just ignore it
|
||||
if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
opts.Height = heightSpec{}
|
||||
}
|
||||
|
||||
if err := opts.initProfiling(); err != nil {
|
||||
return errors.New("failed to start pprof profiles: " + err.Error())
|
||||
}
|
||||
|
||||
return processScheme(opts)
|
||||
}
|
||||
|
||||
// ParseOptions parses command-line options
|
||||
func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
opts := defaultOptions()
|
||||
index := 0
|
||||
|
||||
if useDefaults {
|
||||
// 1. Options from $FZF_DEFAULT_OPTS_FILE
|
||||
@@ -2609,7 +2933,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return nil, errors.New(path + ": " + parseErr.Error())
|
||||
}
|
||||
if len(words) > 0 {
|
||||
if err := parseOptions(opts, words); err != nil {
|
||||
if err := parseOptions(&index, opts, words); err != nil {
|
||||
return nil, errors.New(path + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
@@ -2621,20 +2945,36 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
|
||||
}
|
||||
if len(words) > 0 {
|
||||
if err := parseOptions(opts, words); err != nil {
|
||||
if err := parseOptions(&index, opts, words); err != nil {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Options from command-line arguments
|
||||
if err := parseOptions(opts, args); err != nil {
|
||||
if err := parseOptions(&index, opts, args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := opts.initProfiling(); err != nil {
|
||||
return nil, errors.New("failed to start pprof profiles: " + err.Error())
|
||||
// 4. Final validation of merged options
|
||||
if err := validateOptions(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (opts *Options) reloadOnStart() bool {
|
||||
// Not compatible with --filter
|
||||
if opts.Filter != nil {
|
||||
return false
|
||||
}
|
||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||
for _, action := range actions {
|
||||
if action.t == actReload || action.t == actReloadSync {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIrrelevantNth(t *testing.T) {
|
||||
index := 0
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words := []string{"--nth", "..", "-x"}
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -118,7 +119,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words = append(words, "-x")
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 2 {
|
||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||
@@ -335,10 +336,11 @@ func TestColorSpec(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefaultCtrlNP(t *testing.T) {
|
||||
index := 0
|
||||
check := func(words []string, et tui.EventType, expected actionType) {
|
||||
e := et.AsEvent()
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[e][0].t != expected {
|
||||
t.Error()
|
||||
@@ -364,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
}
|
||||
|
||||
func optsFor(words ...string) *Options {
|
||||
index := 0
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
@@ -451,7 +454,6 @@ func TestValidateSign(t *testing.T) {
|
||||
{"> ", true},
|
||||
{"아", true},
|
||||
{"😀", true},
|
||||
{"", false},
|
||||
{">>>", false},
|
||||
}
|
||||
|
||||
|
@@ -155,14 +155,14 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||
str = strings.ReplaceAll(str, "\\ ", "\t")
|
||||
tokens := _splitRegex.Split(str, -1)
|
||||
sets := []termSet{}
|
||||
set := termSet{}
|
||||
switchSet := false
|
||||
afterBar := false
|
||||
for _, token := range tokens {
|
||||
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
|
||||
lowerText := strings.ToLower(text)
|
||||
caseSensitive := caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && text != lowerText
|
||||
|
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
|
||||
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
146
src/proxy.go
Normal file
146
src/proxy.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const becomeSuffix = ".become"
|
||||
|
||||
func escapeSingleQuote(str string) string {
|
||||
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func fifo(name string) (string, error) {
|
||||
ns := time.Now().UnixNano()
|
||||
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
|
||||
output, err := mkfifo(output, 0600)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
|
||||
output, err := fifo("proxy-output")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
defer os.Remove(output)
|
||||
|
||||
// Take the output
|
||||
go func() {
|
||||
withOutputPipe(output, func(outputFile io.ReadCloser) {
|
||||
if opts.Output == nil {
|
||||
io.Copy(os.Stdout, outputFile)
|
||||
} else {
|
||||
reader := bufio.NewReader(outputFile)
|
||||
sep := opts.PrintSep[0]
|
||||
for {
|
||||
item, err := reader.ReadString(sep)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
opts.Output <- item
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
var command string
|
||||
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
|
||||
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
|
||||
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
|
||||
} else {
|
||||
input, err := fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
defer os.Remove(input)
|
||||
|
||||
go func() {
|
||||
withInputPipe(input, func(inputFile io.WriteCloser) {
|
||||
if opts.Input == nil {
|
||||
io.Copy(inputFile, os.Stdin)
|
||||
} else {
|
||||
for item := range opts.Input {
|
||||
fmt.Fprint(inputFile, item+opts.PrintSep)
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
if withExports {
|
||||
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
|
||||
} else {
|
||||
// For mintty: cannot directly read named pipe from Go code
|
||||
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
|
||||
}
|
||||
}
|
||||
|
||||
// To ensure that the options are processed by a POSIX-compliant shell,
|
||||
// we need to write the command to a temporary file and execute it with sh.
|
||||
var exports []string
|
||||
if withExports {
|
||||
exports = os.Environ()
|
||||
for idx, pairStr := range exports {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
|
||||
}
|
||||
}
|
||||
temp := WriteTemporaryFile(append(exports, command), "\n")
|
||||
defer os.Remove(temp)
|
||||
|
||||
cmd := cmdBuilder(temp)
|
||||
cmd.Stderr = os.Stderr
|
||||
intChan := make(chan os.Signal, 1)
|
||||
defer close(intChan)
|
||||
go func() {
|
||||
if sig, valid := <-intChan; valid {
|
||||
cmd.Process.Signal(sig)
|
||||
}
|
||||
}()
|
||||
signal.Notify(intChan, os.Interrupt)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
code := exitError.ExitCode()
|
||||
if code == ExitBecome {
|
||||
becomeFile := temp + becomeSuffix
|
||||
data, err := os.ReadFile(becomeFile)
|
||||
os.Remove(becomeFile)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
elems := strings.Split(string(data), "\x00")
|
||||
if len(elems) < 1 {
|
||||
return ExitError, errors.New("invalid become command")
|
||||
}
|
||||
command := elems[0]
|
||||
env := []string{}
|
||||
if len(elems) > 1 {
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn()
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
return ExitOk, nil
|
||||
}
|
38
src/proxy_unix.go
Normal file
38
src/proxy_unix.go
Normal file
@@ -0,0 +1,38 @@
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func sh() (string, error) {
|
||||
return "sh", nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
return path, unix.Mkfifo(path, mode)
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(outputFile)
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
return nil
|
||||
}
|
81
src/proxy_windows.go
Normal file
81
src/proxy_windows.go
Normal file
@@ -0,0 +1,81 @@
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var shPath atomic.Value
|
||||
|
||||
func sh() (string, error) {
|
||||
if cached := shPath.Load(); cached != nil {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sh := strings.TrimSpace(string(bytes))
|
||||
shPath.Store(sh)
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
m := strconv.FormatUint(uint64(mode), 8)
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return path, err
|
||||
}
|
||||
return path + ".lnk", nil
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
|
||||
outputFile, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task(outputFile)
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
|
||||
inputFile, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -25,6 +26,7 @@ type Reader struct {
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
execOut io.ReadCloser
|
||||
command *string
|
||||
killed bool
|
||||
wait bool
|
||||
@@ -32,7 +34,7 @@ type Reader struct {
|
||||
|
||||
// NewReader returns new Reader object
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
||||
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
|
||||
}
|
||||
|
||||
func (r *Reader) startEventPoller() {
|
||||
@@ -79,6 +81,7 @@ func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
r.execOut.Close()
|
||||
util.KillCommand(r.exec)
|
||||
} else {
|
||||
os.Stdin.Close()
|
||||
@@ -113,7 +116,7 @@ func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts,
|
||||
var success bool
|
||||
if inputChan != nil {
|
||||
success = r.readChannel(inputChan)
|
||||
} else if util.IsTty() {
|
||||
} else if util.IsTty(os.Stdin) {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
success = r.readFiles(root, opts, ignores)
|
||||
@@ -220,17 +223,45 @@ func (r *Reader) readFromStdin() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isSymlinkToDir(path string, de os.DirEntry) bool {
|
||||
if de.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
return s.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
bytes := stringBytes(path)
|
||||
|
||||
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
|
||||
bytes = bytes[2:]
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return "."
|
||||
}
|
||||
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
r.killed = false
|
||||
conf := fastwalk.Config{Follow: opts.follow}
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
path = trimPath(path)
|
||||
if path != "." {
|
||||
isDir := de.IsDir()
|
||||
if isDir {
|
||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
@@ -241,7 +272,7 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
@@ -263,16 +294,23 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
|
||||
if environ != nil {
|
||||
r.exec.Env = environ
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
|
||||
var err error
|
||||
r.execOut, err = r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
err = r.exec.Start()
|
||||
r.mutex.Unlock()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
r.feed(out)
|
||||
|
||||
r.mutex.Unlock()
|
||||
r.feed(r.execOut)
|
||||
return r.exec.Wait() == nil
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ type Offset [2]int32
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -80,7 +81,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
@@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase)})
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
match: false})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -38,9 +38,9 @@ const (
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
responseChannel chan string
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
getHandler func(getParams) string
|
||||
}
|
||||
|
||||
type listenAddress struct {
|
||||
@@ -73,7 +73,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
||||
return listenAddress{parts[0], port}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
@@ -99,9 +99,9 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
responseChannel: responseChannel,
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -165,17 +165,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
case 0:
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
||||
select {
|
||||
case response := <-server.responseChannel:
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
case <-time.After(channelTimeout):
|
||||
go func() {
|
||||
// Drain the channel
|
||||
<-server.responseChannel
|
||||
}()
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
|
1261
src/terminal.go
1261
src/terminal.go
File diff suppressed because it is too large
Load Diff
57
src/tmux.go
Normal file
57
src/tmux.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func runTmux(args []string, opts *Options) (int, error) {
|
||||
// Prepare arguments
|
||||
fzf := args[0]
|
||||
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
|
||||
if opts.BorderShape == tui.BorderUndefined {
|
||||
args = append(args, "--border")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range args {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-tmux --no-height`
|
||||
|
||||
// Get current directory
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
|
||||
// Set tmux options for popup placement
|
||||
// C Both The centre of the terminal
|
||||
// R -x The right side of the terminal
|
||||
// P Both The bottom left of the pane
|
||||
// M Both The mouse position
|
||||
// W Both The window position on the status line
|
||||
// S -y The line above or below the status line
|
||||
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
|
||||
switch opts.Tmux.position {
|
||||
case posUp:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||
case posDown:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-yS")
|
||||
case posLeft:
|
||||
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
|
||||
case posRight:
|
||||
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
|
||||
case posCenter:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
|
||||
}
|
||||
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
|
||||
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
sh, _ := sh()
|
||||
tmuxArgs = append(tmuxArgs, sh, temp)
|
||||
return exec.Command("tmux", tmuxArgs...)
|
||||
}, opts, true)
|
||||
}
|
@@ -107,11 +107,12 @@ func _() {
|
||||
_ = x[Result-96]
|
||||
_ = x[Jump-97]
|
||||
_ = x[JumpCancel-98]
|
||||
_ = x[ClickHeader-99]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
@@ -73,7 +73,7 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ type LightRenderer struct {
|
||||
prevDownTime time.Time
|
||||
clicks [][2]int
|
||||
ttyin *os.File
|
||||
ttyout *os.File
|
||||
buffer []byte
|
||||
origState *term.State
|
||||
width int
|
||||
@@ -126,10 +127,10 @@ type LightWindow struct {
|
||||
bg Color
|
||||
}
|
||||
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
in, err := openTtyIn()
|
||||
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
out, err := openTtyOut()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
out = os.Stderr
|
||||
}
|
||||
r := LightRenderer{
|
||||
closed: util.NewAtomicBool(false),
|
||||
@@ -137,7 +138,8 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
clearOnExit: clearOnExit,
|
||||
ttyin: in,
|
||||
ttyin: ttyin,
|
||||
ttyout: out,
|
||||
yoffset: 0,
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
@@ -792,6 +794,9 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
||||
w.fg = r.theme.Fg.Color
|
||||
w.bg = r.theme.Bg.Color
|
||||
}
|
||||
if !w.bg.IsDefault() && w.border.shape != BorderNone {
|
||||
w.Erase()
|
||||
}
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
@@ -1019,7 +1024,7 @@ func (w *LightWindow) Print(text string) {
|
||||
}
|
||||
|
||||
func cleanse(str string) string {
|
||||
return strings.Replace(str, "\x1b", "", -1)
|
||||
return strings.ReplaceAll(str, "\x1b", "")
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
|
@@ -33,27 +33,21 @@ func (r *LightRenderer) fd() int {
|
||||
return int(r.ttyin.Fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
fd := r.fd()
|
||||
origState, err := term.GetState(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.origState = origState
|
||||
term.MakeRaw(fd)
|
||||
return nil
|
||||
func (r *LightRenderer) initPlatform() (err error) {
|
||||
r.origState, err = term.MakeRaw(r.fd())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *LightRenderer) closePlatform() {
|
||||
// NOOP
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
func openTty(mode int) (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, mode, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
@@ -62,6 +56,14 @@ func openTtyIn() (*os.File, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
return openTty(syscall.O_RDONLY)
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return openTty(syscall.O_WRONLY)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
term.MakeRaw(r.fd())
|
||||
}
|
||||
|
@@ -96,6 +96,10 @@ func openTtyIn() (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
@@ -135,7 +139,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
||||
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
|
@@ -3,6 +3,7 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
|
||||
|
||||
// Test the handling of the tcell keyboard events.
|
||||
func TestGetCharEventKey(t *testing.T) {
|
||||
if util.ToTty() {
|
||||
if util.IsTty(os.Stdout) {
|
||||
// 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
|
||||
|
@@ -4,12 +4,19 @@ package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||
|
||||
var tty atomic.Value
|
||||
|
||||
func ttyname() string {
|
||||
if cached := tty.Load(); cached != nil {
|
||||
return cached.(string)
|
||||
}
|
||||
|
||||
var stderr syscall.Stat_t
|
||||
if syscall.Fstat(2, &stderr) != nil {
|
||||
return ""
|
||||
@@ -27,24 +34,21 @@ func ttyname() string {
|
||||
continue
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
return prefix + file.Name()
|
||||
value := prefix + file.Name()
|
||||
tty.Store(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
}
|
||||
}
|
||||
return os.Stdin
|
||||
}
|
||||
return in
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn() (*os.File, error) {
|
||||
return openTtyIn()
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to write to
|
||||
func TtyOut() (*os.File, error) {
|
||||
return openTtyOut()
|
||||
}
|
||||
|
@@ -2,13 +2,20 @@
|
||||
|
||||
package tui
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func ttyname() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
return os.Stdin
|
||||
func TtyIn() (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns nil
|
||||
func TtyOut() (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@@ -334,15 +334,6 @@ type Event struct {
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
func (e Event) Is(types ...EventType) bool {
|
||||
for _, t := range types {
|
||||
if e.Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
@@ -356,7 +347,8 @@ type MouseEvent struct {
|
||||
type BorderShape int
|
||||
|
||||
const (
|
||||
BorderNone BorderShape = iota
|
||||
BorderUndefined BorderShape = iota
|
||||
BorderNone
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
@@ -701,9 +693,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
@@ -731,9 +723,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
@@ -761,9 +753,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
@@ -822,7 +814,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
// These colors are not defined in the base themes
|
||||
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
|
||||
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
|
||||
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
|
||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
|
||||
return chars.slice
|
||||
}
|
||||
|
||||
func (chars *Chars) NumLines(atMost int) (int, bool) {
|
||||
lines := 1
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
for _, r := range runes {
|
||||
if r == '\n' {
|
||||
lines++
|
||||
}
|
||||
if lines > atMost {
|
||||
return atMost, true
|
||||
}
|
||||
}
|
||||
return lines, false
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(chars.slice); idx++ {
|
||||
found := bytes.IndexByte(chars.slice[idx:], '\n')
|
||||
if found < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
idx += found
|
||||
lines++
|
||||
if lines > atMost {
|
||||
return atMost, true
|
||||
}
|
||||
}
|
||||
return lines, false
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
@@ -196,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
|
||||
chars.slice = append([]byte(prefix), chars.slice...)
|
||||
}
|
||||
}
|
||||
|
||||
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
|
||||
text := make([]rune, chars.Length())
|
||||
copy(text, chars.ToRunes())
|
||||
|
||||
lines := [][]rune{}
|
||||
overflow := false
|
||||
if !multiLine {
|
||||
lines = append(lines, text)
|
||||
} else {
|
||||
from := 0
|
||||
for off := 0; off < len(text); off++ {
|
||||
if text[off] == '\n' {
|
||||
lines = append(lines, text[from:off+1]) // Include '\n'
|
||||
from = off + 1
|
||||
if len(lines) >= maxLines {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lastLine []rune
|
||||
if from < len(text) {
|
||||
lastLine = text[from:]
|
||||
}
|
||||
|
||||
overflow = false
|
||||
if len(lines) >= maxLines {
|
||||
overflow = true
|
||||
} else {
|
||||
lines = append(lines, lastLine)
|
||||
}
|
||||
}
|
||||
|
||||
// If wrapping is disabled, we're done
|
||||
if wrapCols == 0 {
|
||||
return lines, overflow
|
||||
}
|
||||
|
||||
wrapped := [][]rune{}
|
||||
for _, line := range lines {
|
||||
// Remove trailing '\n' and remember if it was there
|
||||
newline := len(line) > 0 && line[len(line)-1] == '\n'
|
||||
if newline {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
for {
|
||||
cols := wrapCols
|
||||
if len(wrapped) > 0 {
|
||||
cols -= wrapSignWidth
|
||||
}
|
||||
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||
if overflowIdx >= 0 {
|
||||
// Might be a wide character
|
||||
if overflowIdx == 0 {
|
||||
overflowIdx = 1
|
||||
}
|
||||
if len(wrapped) >= maxLines {
|
||||
return wrapped, true
|
||||
}
|
||||
wrapped = append(wrapped, line[:overflowIdx])
|
||||
line = line[overflowIdx:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Restore trailing '\n'
|
||||
if newline {
|
||||
line = append(line, '\n')
|
||||
}
|
||||
|
||||
if len(wrapped) >= maxLines {
|
||||
return wrapped, true
|
||||
}
|
||||
|
||||
wrapped = append(wrapped, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return wrapped, false
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
@@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
|
||||
func TestCharsLines(t *testing.T) {
|
||||
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
||||
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
||||
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
|
||||
fmt.Println(lines, overflow)
|
||||
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
||||
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
// No wrap
|
||||
check(true, 1, 0, 0, 8, 1, true)
|
||||
check(true, 2, 0, 0, 8, 2, true)
|
||||
check(true, 3, 0, 0, 8, 3, false)
|
||||
|
||||
// Wrap (2)
|
||||
check(true, 4, 2, 0, 8, 4, true)
|
||||
check(true, 5, 2, 0, 8, 5, true)
|
||||
check(true, 6, 2, 0, 8, 6, true)
|
||||
check(true, 7, 2, 0, 8, 7, true)
|
||||
check(true, 8, 2, 0, 8, 8, true)
|
||||
check(true, 9, 2, 0, 8, 9, false)
|
||||
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
|
||||
|
||||
// With wrap sign (3 + 1)
|
||||
check(true, 100, 3, 1, 1, 8, false)
|
||||
|
||||
// With wrap sign (3 + 2)
|
||||
check(true, 100, 3, 2, 1, 12, false)
|
||||
|
||||
// With wrap sign (3 + 2) and no multi-line
|
||||
check(false, 100, 3, 2, 1, 13, false)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -137,14 +138,20 @@ func DurWithin(
|
||||
return val
|
||||
}
|
||||
|
||||
// IsTty returns true if stdin is a terminal
|
||||
func IsTty() bool {
|
||||
return isatty.IsTerminal(os.Stdin.Fd())
|
||||
// IsTty returns true if the file is a terminal
|
||||
func IsTty(file *os.File) bool {
|
||||
fd := file.Fd()
|
||||
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
|
||||
}
|
||||
|
||||
// ToTty returns true if stdout is a terminal
|
||||
func ToTty() bool {
|
||||
return isatty.IsTerminal(os.Stdout.Fd())
|
||||
// RunOnce runs the given function only once
|
||||
func RunOnce(f func()) func() {
|
||||
once := Once(true)
|
||||
return func() {
|
||||
if once() {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once returns a function that returns the specified boolean value only once
|
||||
@@ -152,7 +159,7 @@ func Once(nextResponse bool) func() bool {
|
||||
state := nextResponse
|
||||
return func() bool {
|
||||
prevState := state
|
||||
state = false
|
||||
state = !nextResponse
|
||||
return prevState
|
||||
}
|
||||
}
|
||||
@@ -188,3 +195,34 @@ func ToKebabCase(s string) string {
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// CompareVersions compares two version strings
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
atoi := func(s string) int {
|
||||
n, e := strconv.Atoi(s)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = atoi(parts1[i])
|
||||
}
|
||||
if i < len(parts2) {
|
||||
p2 = atoi(parts2[i])
|
||||
}
|
||||
|
||||
if p1 > p2 {
|
||||
return 1
|
||||
} else if p1 < p2 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@@ -137,8 +137,11 @@ func TestOnce(t *testing.T) {
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
if !o() {
|
||||
t.Error("Expected: true")
|
||||
}
|
||||
if !o() {
|
||||
t.Error("Expected: true")
|
||||
}
|
||||
|
||||
o = Once(true)
|
||||
@@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunesWidth(t *testing.T) {
|
||||
@@ -203,3 +209,34 @@ func TestStringWidth(t *testing.T) {
|
||||
t.Errorf("Expected: %d, Actual: %d", 1, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
assert := func(a, b string, expected int) {
|
||||
if result := CompareVersions(a, b); result != expected {
|
||||
t.Errorf("Expected: %d, Actual: %d", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
assert("2", "1", 1)
|
||||
assert("2", "2", 0)
|
||||
assert("2", "10", -1)
|
||||
|
||||
assert("2.1", "2.2", -1)
|
||||
assert("2.1", "2.1.1", -1)
|
||||
|
||||
assert("1.2.3", "1.2.2", 1)
|
||||
assert("1.2.3", "1.2.3", 0)
|
||||
assert("1.2.3", "1.2.3.0", 0)
|
||||
assert("1.2.3", "1.2.4", -1)
|
||||
|
||||
// Different number of parts
|
||||
assert("1.0.0", "1", 0)
|
||||
assert("1.0.0", "1.0", 0)
|
||||
assert("1.0.0", "1.0.0", 0)
|
||||
assert("1.0", "1.0.0", 0)
|
||||
assert("1", "1.0.0", 0)
|
||||
assert("1.0.0", "1.0.0.1", -1)
|
||||
assert("1.0.0.1.0", "1.0.0.1", 0)
|
||||
|
||||
assert("", "3.4.5", -1)
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@@ -20,6 +21,8 @@ const (
|
||||
shellTypePowerShell
|
||||
)
|
||||
|
||||
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
|
||||
|
||||
type Executor struct {
|
||||
shell string
|
||||
shellType shellType
|
||||
@@ -131,7 +134,9 @@ func escapeArg(s string) string {
|
||||
b = append(b, '\\')
|
||||
}
|
||||
b = append(b, '"')
|
||||
return string(b)
|
||||
return escapeRegex.ReplaceAllStringFunc(string(b), func(match string) string {
|
||||
return "^" + match
|
||||
})
|
||||
}
|
||||
|
||||
func (x *Executor) QuoteEntry(entry string) string {
|
||||
@@ -152,10 +157,10 @@ func (x *Executor) QuoteEntry(entry string) string {
|
||||
*/
|
||||
return escapeArg(entry)
|
||||
case shellTypePowerShell:
|
||||
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
||||
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
||||
default:
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
|
||||
}
|
||||
}
|
||||
|
||||
|
13
src/winpty.go
Normal file
13
src/winpty.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import "errors"
|
||||
|
||||
func needWinpty(_ *Options) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func runWinpty(_ []string, _ *Options) (int, error) {
|
||||
return ExitError, errors.New("Not supported")
|
||||
}
|
75
src/winpty_windows.go
Normal file
75
src/winpty_windows.go
Normal file
@@ -0,0 +1,75 @@
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func isMintty345() bool {
|
||||
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
|
||||
}
|
||||
|
||||
func needWinpty(opts *Options) bool {
|
||||
if os.Getenv("TERM_PROGRAM") != "mintty" {
|
||||
return false
|
||||
}
|
||||
if isMintty345() {
|
||||
/*
|
||||
See: https://github.com/junegunn/fzf/issues/3809
|
||||
|
||||
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
|
||||
*/
|
||||
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Setting the environment variable here unfortunately doesn't help,
|
||||
// so we need to start a child process with "MSYS=enable_pcon"
|
||||
// os.Setenv("MSYS", "enable_pcon")
|
||||
return true
|
||||
}
|
||||
if opts.NoWinpty {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("winpty"); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func runWinpty(args []string, opts *Options) (int, error) {
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
|
||||
argStr := escapeSingleQuote(args[0])
|
||||
for _, arg := range args[1:] {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-winpty`
|
||||
|
||||
if isMintty345() {
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
cmd := exec.Command(sh, temp)
|
||||
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
}, opts, false)
|
||||
}
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
}, opts, false)
|
||||
}
|
415
test/test_go.rb
415
test/test_go.rb
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user