mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-31 20:22:01 -07:00
Compare commits
115 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
091b7eacba | ||
|
e74b1251c0 | ||
|
d282a1649d | ||
|
6ce8d49d1b | ||
|
c5b197078a | ||
|
0494f20d62 | ||
|
73aff476dd | ||
|
98ee5e651a | ||
|
01871ea383 | ||
|
1dbdb9438f | ||
|
c70f0eadb8 | ||
|
26244ad8c2 | ||
|
fa0aa5510d | ||
|
eec557b6aa | ||
|
0cc27c3cc1 | ||
|
507089d7b2 | ||
|
a6b3517b75 | ||
|
2d6beb7813 | ||
|
61bc129e1d | ||
|
52210a57f0 | ||
|
8061a2f108 | ||
|
7444eff6d4 | ||
|
f35a9da99a | ||
|
c3098e9ab2 | ||
|
686f9288fc | ||
|
1833670fb9 | ||
|
3dd42f5aa2 | ||
|
99a7beba57 | ||
|
edee2b753c | ||
|
545d5770be | ||
|
ca747a2b54 | ||
|
17da165cfe | ||
|
5e6788c679 | ||
|
425deadca9 | ||
|
2c8e9dd3a5 | ||
|
7a72f1a253 | ||
|
208e556332 | ||
|
c65d11bfb5 | ||
|
3b5b52d89a | ||
|
a4f6c8f990 | ||
|
670c329852 | ||
|
f3551c8422 | ||
|
90b8187882 | ||
|
1a43259989 | ||
|
3c0a630475 | ||
|
2a1e5a9729 | ||
|
413c66beba | ||
|
1416e696b1 | ||
|
d373cf89c7 | ||
|
dd886d22f0 | ||
|
472569a27c | ||
|
76cf6559cc | ||
|
a34e8dcdc9 | ||
|
da752fc9a4 | ||
|
beb2de2dd9 | ||
|
2a8b65e105 | ||
|
62a916bc24 | ||
|
c47b833e7b | ||
|
09b0958b5f | ||
|
3a4c3d3e58 | ||
|
7484292e63 | ||
|
687c2741b8 | ||
|
2fb285e530 | ||
|
16f6473938 | ||
|
66546208b2 | ||
|
532274045e | ||
|
9347c72fb6 | ||
|
e90bb7169c | ||
|
8a2c41e183 | ||
|
59fb65293a | ||
|
e7718b92b7 | ||
|
cdfaf761df | ||
|
1a9ea6f738 | ||
|
945c1c8597 | ||
|
e4d0f7acd5 | ||
|
250496c953 | ||
|
e47dc758c9 | ||
|
b92a843c5f | ||
|
91bea9c5b3 | ||
|
d75bb5cbe1 | ||
|
2671259fdb | ||
|
2024010119 | ||
|
412040f77e | ||
|
d210660ce8 | ||
|
863a12562b | ||
|
5da606a9ac | ||
|
8d20f3d5c4 | ||
|
5d360180af | ||
|
f0fbed6007 | ||
|
519de7c833 | ||
|
97ccef1a04 | ||
|
c4df0dd06e | ||
|
cd114c6818 | ||
|
1707b8cdba | ||
|
41d4d70b98 | ||
|
0e999482cb | ||
|
65b2c06027 | ||
|
d7b61ede07 | ||
|
87fc1c84b8 | ||
|
d4b5f12383 | ||
|
eb62b0d665 | ||
|
91387a741b | ||
|
e8b34cb00d | ||
|
82954258c1 | ||
|
50f092551b | ||
|
c36a64be68 | ||
|
a343b20775 | ||
|
a714e76ae1 | ||
|
d21d5c9510 | ||
|
cd6788a2bb | ||
|
6b99399c41 | ||
|
952b6af445 | ||
|
7c674ad7fa | ||
|
d7d2ac3951 | ||
|
29e67d307a |
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -27,18 +27,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
4
.github/workflows/depsreview.yaml
vendored
4
.github/workflows/depsreview.yaml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
12
.github/workflows/linux.yml
vendored
12
.github/workflows/linux.yml
vendored
@@ -11,16 +11,19 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
@@ -42,4 +45,7 @@ jobs:
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
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
|
||||
|
4
.github/workflows/macos.yml
vendored
4
.github/workflows/macos.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
|
2
.github/workflows/sponsors.yml
vendored
2
.github/workflows/sponsors.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
|
4
.github/workflows/typos.yml
vendored
4
.github/workflows/typos.yml
vendored
@@ -6,5 +6,5 @@ jobs:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: crate-ci/typos@v1.16.4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.19.0
|
||||
|
@@ -1 +1 @@
|
||||
golang 1.20.4
|
||||
golang 1.20.13
|
||||
|
74
ADVANCED.md
74
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2023/05/26*
|
||||
* *Requires fzf 0.41.0 or above*
|
||||
* *Last update: 2024/01/20*
|
||||
* *Requires fzf 0.46.0 or above*
|
||||
|
||||
---
|
||||
|
||||
@@ -16,17 +16,20 @@ Advanced fzf examples
|
||||
* [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)
|
||||
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Color themes](#color-themes)
|
||||
* [fzf Theme Playground](#fzf-theme-playground)
|
||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
@@ -208,6 +211,30 @@ find * | fzf --prompt 'All> ' \
|
||||
|
||||

|
||||
|
||||
### Toggling with a single key binding
|
||||
|
||||
The above example uses two different key bindings to toggle between two modes,
|
||||
but can we just use a single key binding?
|
||||
|
||||
To make a key binding behave differently each time it is pressed, we need:
|
||||
|
||||
1. a way to store the current state. i.e. "which mode are we in?"
|
||||
2. and a way to dynamically perform different actions depending on the state.
|
||||
|
||||
The following example shows how to 1. store the current mode in the prompt
|
||||
string, 2. and use this information (`$FZF_PROMPT`) to determine which
|
||||
actions to perform using the `transform` action.
|
||||
|
||||
```sh
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
|
||||
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
|
||||
```
|
||||
|
||||
Ripgrep integration
|
||||
-------------------
|
||||
|
||||
@@ -442,6 +469,41 @@ INITIAL_QUERY="${*:-}"
|
||||
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
|
||||
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode using a single key binding
|
||||
|
||||
In contrast to the previous version, we use just one hotkey to toggle between
|
||||
ripgrep and fzf mode. This is achieved by using the `$FZF_PROMPT` as a state
|
||||
within the `transform` action, a feature introduced in [fzf 0.45.0][0.45.0]. A
|
||||
more detailed explanation of this feature can be found in a previous section -
|
||||
[Toggling with a single keybinding](#toggling-with-a-single-key-binding).
|
||||
|
||||
[0.45.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0450
|
||||
|
||||
When using the `transform` action, the placeholder (`\{q}`) should be escaped to
|
||||
prevent immediate evaluation.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
|
||||
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" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
|
||||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header 'CTRL-T: Switch between ripgrep/fzf' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -490,7 +552,7 @@ pods() {
|
||||
- Press enter key on a pod to `kubectl exec` into it
|
||||
- Press CTRL-O to open the log in your editor
|
||||
- Press CTRL-R to reload the pod list
|
||||
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
|
||||
- Press CTRL-/ repeatedly to rotate through a different sets of preview
|
||||
window options
|
||||
1. `80%,border-bottom`
|
||||
1. `hidden`
|
||||
@@ -571,6 +633,12 @@ export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#
|
||||
|
||||

|
||||
|
||||
### fzf Theme Playground
|
||||
|
||||
[fzf Theme Playground](https://vitormv.github.io/fzf-themes/) created by
|
||||
[Vitor Mello](https://github.com/vitormv) is a webpage where you can
|
||||
interactively create fzf themes.
|
||||
|
||||
### Generating fzf color theme from Vim color schemes
|
||||
|
||||
The Vim plugin of fzf can generate `--color` option from the current color
|
||||
|
6
BUILD.md
6
BUILD.md
@@ -34,14 +34,16 @@ make release
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [rivo/uniseg](https://github.com/rivo/uniseg)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
- [fastwalk](https://github.com/charlievieth/fastwalk)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/charlievieth/fastwalk/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
|
173
CHANGELOG.md
173
CHANGELOG.md
@@ -1,6 +1,177 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.48.0
|
||||
------
|
||||
- Shell integration scripts are now embedded in the fzf binary. This simplifies the distribution, and the users are less likely to have problems caused by using incompatible scripts and binaries.
|
||||
- bash
|
||||
```sh
|
||||
# Set up fzf key bindings and fuzzy completion
|
||||
eval "$(fzf --bash)"
|
||||
```
|
||||
- zsh
|
||||
```sh
|
||||
# Set up fzf key bindings and fuzzy completion
|
||||
eval "$(fzf --zsh)"
|
||||
```
|
||||
- fish
|
||||
```fish
|
||||
# Set up fzf key bindings
|
||||
fzf --fish | source
|
||||
```
|
||||
- Added options for customizing the behavior of the built-in walker
|
||||
| Option | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `--walker=OPTS` | Walker options (`[file][,dir][,follow][,hidden]`) | `file,follow,hidden` |
|
||||
| `--walker-root=DIR` | Root directory from which to start walker | `.` |
|
||||
| `--walker-skip=DIRS` | Comma-separated list of directory names to skip | `.git,node_modules` |
|
||||
- Examples
|
||||
```sh
|
||||
# Built-in walker is only used by standalone fzf when $FZF_DEFAULT_COMMAND is not set
|
||||
unset FZF_DEFAULT_COMMAND
|
||||
|
||||
fzf # default: --walker=file,follow,hidden --walker-root=. --walker-skip=.git,node_modules
|
||||
fzf --walker=file,dir,hidden,follow --walker-skip=.git,node_modules,target
|
||||
|
||||
# Walker options in $FZF_DEFAULT_OPTS
|
||||
export FZF_DEFAULT_OPTS="--walker=file,dir,hidden,follow --walker-skip=.git,node_modules,target"
|
||||
fzf
|
||||
|
||||
# Reading from STDIN; --walker is ignored
|
||||
seq 100 | fzf --walker=dir
|
||||
|
||||
# Reading from $FZF_DEFAULT_COMMAND; --walker is ignored
|
||||
export FZF_DEFAULT_COMMAND='seq 100'
|
||||
fzf --walker=dir
|
||||
```
|
||||
- Shell integration scripts have been updated to use the built-in walker with these new options and they are now much faster out of the box.
|
||||
|
||||
0.47.0
|
||||
------
|
||||
- Replaced ["the default find command"][find] with a built-in directory walker to simplify the code and to achieve better performance and consistent behavior across platforms.
|
||||
This doesn't affect you if you have `$FZF_DEFAULT_COMMAND` set.
|
||||
- Breaking changes:
|
||||
- Unlike [the previous "find" command][find], the new traversal code will list hidden files, but hidden directories will still be ignored
|
||||
- No filtering of `devtmpfs` or `proc` types
|
||||
- Traversal is parallelized, so the order of the entries will be different each time
|
||||
- You may wonder why fzf implements directory walker anyway when it's a filter program following the [Unix philosophy][unix].
|
||||
But fzf has had [the walker code for years][walker] to tackle the performance problem on Windows. And I decided to use the same approach on different platforms as well for the benefits listed above.
|
||||
- Built-in walker is using the excellent [charlievieth/fastwalk][fastwalk] library, which easily outperforms its competitors and supports safely following symlinks.
|
||||
- Added `$FZF_DEFAULT_OPTS_FILE` to allow managing default options in a file
|
||||
- See [#3618](https://github.com/junegunn/fzf/pull/3618)
|
||||
- Option precedence from lower to higher
|
||||
1. Options read from `$FZF_DEFAULT_OPTS_FILE`
|
||||
1. Options from `$FZF_DEFAULT_OPTS`
|
||||
1. Options from command-line arguments
|
||||
- Bug fixes and improvements
|
||||
|
||||
[find]: https://github.com/junegunn/fzf/blob/0.46.1/src/constants.go#L60-L64
|
||||
[walker]: https://github.com/junegunn/fzf/pull/1847
|
||||
[fastwalk]: https://github.com/charlievieth/fastwalk
|
||||
[unix]: https://en.wikipedia.org/wiki/Unix_philosophy
|
||||
|
||||
0.46.1
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- Fixed Windows binaries
|
||||
- Downgraded Go version to 1.20 to support older versions of Windows
|
||||
- https://tip.golang.org/doc/go1.21#windows
|
||||
- Updated [rivo/uniseg](https://github.com/rivo/uniseg) dependency to v0.4.6
|
||||
|
||||
0.46.0
|
||||
------
|
||||
- Added two new events
|
||||
- `result` - triggered when the filtering for the current query is complete and the result list is ready
|
||||
- `resize` - triggered when the terminal size is changed
|
||||
- fzf now exports the following environment variables to the child processes
|
||||
| Variable | Description |
|
||||
| --- | --- |
|
||||
| `FZF_LINES` | Number of lines fzf takes up excluding padding and margin |
|
||||
| `FZF_COLUMNS` | Number of columns fzf takes up excluding padding and margin |
|
||||
| `FZF_TOTAL_COUNT` | Total number of items |
|
||||
| `FZF_MATCH_COUNT` | Number of matched items |
|
||||
| `FZF_SELECT_COUNT` | Number of selected items |
|
||||
| `FZF_QUERY` | Current query string |
|
||||
| `FZF_PROMPT` | Prompt string |
|
||||
| `FZF_ACTION` | The name of the last action performed |
|
||||
- This allows you to write sophisticated transformations like so
|
||||
```sh
|
||||
# Script to dynamically resize the preview window
|
||||
transformer='
|
||||
# 1 line for info, another for prompt, and 2 more lines for preview window border
|
||||
lines=$(( FZF_LINES - FZF_MATCH_COUNT - 4 ))
|
||||
if [[ $FZF_MATCH_COUNT -eq 0 ]]; then
|
||||
echo "change-preview-window:hidden"
|
||||
elif [[ $lines -gt 3 ]]; then
|
||||
echo "change-preview-window:$lines"
|
||||
elif [[ $FZF_PREVIEW_LINES -ne 3 ]]; then
|
||||
echo "change-preview-window:3"
|
||||
fi
|
||||
'
|
||||
seq 10000 | fzf --preview 'seq {} 10000' --preview-window up \
|
||||
--bind "result:transform:$transformer" \
|
||||
--bind "resize:transform:$transformer"
|
||||
```
|
||||
- And we're phasing out `{fzf:prompt}` and `{fzf:action}`
|
||||
- Changed [mattn/go-runewidth](https://github.com/mattn/go-runewidth) dependency to [rivo/uniseg](https://github.com/rivo/uniseg) for accurate results
|
||||
- Set `--ambidouble` if your terminal displays ambiguous width characters (e.g. box-drawing characters for borders) as 2 columns
|
||||
- `RUNEWIDTH_EASTASIAN=1` is still respected for backward compatibility, but it's recommended that you use this new option instead
|
||||
- Bug fixes
|
||||
|
||||
0.45.0
|
||||
------
|
||||
- Added `transform` action to conditionally perform a series of actions
|
||||
```sh
|
||||
# Disallow selecting an empty line
|
||||
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \
|
||||
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
|
||||
|
||||
# Move cursor past the empty line
|
||||
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \
|
||||
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
|
||||
--bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
|
||||
|
||||
# A single key binding to toggle between modes
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"'
|
||||
```
|
||||
- Added placeholder expressions
|
||||
- `{fzf:action}` - The name of the last action performed
|
||||
- `{fzf:prompt}` - Prompt string (including ANSI color codes)
|
||||
- `{fzf:query}` - Synonym for `{q}`
|
||||
- Added support for negative height
|
||||
```sh
|
||||
# Terminal height minus 1, so you can still see the command line
|
||||
fzf --height=-1
|
||||
```
|
||||
- This handles a terminal resize better than `--height=$(($(tput lines) - 1))`
|
||||
- Added `accept-or-print-query` action that acts like `accept` but prints the
|
||||
current query when there's no match for the query
|
||||
```sh
|
||||
# You can make CTRL-R paste the current query when there's no match
|
||||
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'
|
||||
```
|
||||
- Note that there are alternative ways to implement the same strategy
|
||||
```sh
|
||||
# 'become' is apparently more versatile but it's not available on Windows.
|
||||
export FZF_CTRL_R_OPTS='--bind "enter:become:if [ -z {} ]; then echo {q}; else echo {}; fi"'
|
||||
|
||||
# Using the new 'transform' action
|
||||
export FZF_CTRL_R_OPTS='--bind "enter:transform:[ -z {} ] && echo print-query || echo accept"'
|
||||
```
|
||||
- Added `show-header` and `hide-header` actions
|
||||
- Bug fixes
|
||||
|
||||
0.44.1
|
||||
------
|
||||
- Fixed crash when preview window is hidden on `focus` event
|
||||
|
||||
0.44.0
|
||||
------
|
||||
- (Experimental) Sixel image support in preview window (not available on Windows)
|
||||
@@ -38,7 +209,7 @@ CHANGELOG
|
||||
# --transfer-mode=memory is the fastest option but if you want fzf to be able
|
||||
# to redraw the image on terminal resize or on 'change-preview-window',
|
||||
# you need to use --transfer-mode=stream.
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \$d
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \$d
|
||||
else
|
||||
bat --color=always {}
|
||||
fi
|
||||
|
@@ -1,5 +1,5 @@
|
||||
FROM --platform=linux/amd64 archlinux
|
||||
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
|
||||
FROM --platform=linux/amd64 ubuntu:22.04
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
||||
RUN gem install --no-document -v 5.14.2 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
@@ -8,4 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
ENV LANG C.UTF-8
|
||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
9
Makefile
9
Makefile
@@ -89,8 +89,11 @@ bench:
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
generate:
|
||||
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
|
||||
|
||||
build:
|
||||
goreleaser build --rm-dist --snapshot --skip-post-hooks
|
||||
goreleaser build --clean --snapshot --skip=post-hooks
|
||||
|
||||
release:
|
||||
# Make sure that the tests pass and the build works
|
||||
@@ -123,7 +126,7 @@ endif
|
||||
git push origin temp --follow-tags --force
|
||||
|
||||
# Make a GitHub release
|
||||
goreleaser --rm-dist --release-notes tmp/release-note
|
||||
goreleaser --clean --release-notes tmp/release-note
|
||||
|
||||
# Push to master
|
||||
git checkout master
|
||||
@@ -181,4 +184,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all build release test bench install clean docker docker-test update
|
||||
.PHONY: all generate build release test bench install clean docker docker-test update
|
||||
|
@@ -238,19 +238,20 @@ call fzf#run({'sink': 'e'})
|
||||
```
|
||||
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
|
||||
```vim
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
```
|
||||
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
|
||||
```vim
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
@@ -489,4 +490,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
@@ -53,7 +53,7 @@ if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
|
@@ -95,9 +95,9 @@ while [[ $# -gt 0 ]]; do
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
@@ -196,8 +196,9 @@ if [[ "$opt" =~ "-E" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
33
doc/fzf.txt
33
doc/fzf.txt
@@ -1,4 +1,4 @@
|
||||
fzf.txt fzf Last change: September 17 2023
|
||||
fzf.txt fzf Last change: February 15 2024
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -264,17 +264,18 @@ entry.
|
||||
call fzf#run({'sink': 'e'})
|
||||
<
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
>
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
<
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
>
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
<
|
||||
@@ -417,24 +418,12 @@ TIPS *fzf-tips*
|
||||
< fzf inside terminal buffer >________________________________________________~
|
||||
*fzf-inside-terminal-buffer*
|
||||
|
||||
The latest versions of Vim and Neovim include builtin terminal emulator
|
||||
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
|
||||
|
||||
- On Neovim
|
||||
- On GVim
|
||||
- On Terminal Vim with a non-default layout
|
||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
||||
|
||||
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||
If you find the default ANSI colors to be different, consider configuring the
|
||||
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||
in Neovim.
|
||||
|
||||
*g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13*
|
||||
*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9*
|
||||
*g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5*
|
||||
*g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1*
|
||||
*g:terminal_color_0*
|
||||
>
|
||||
" Terminal colors for seoul256 color scheme
|
||||
if has('nvim')
|
||||
@@ -512,7 +501,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
17
go.mod
17
go.mod
@@ -1,21 +1,20 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/charlievieth/fastwalk v1.0.2
|
||||
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.4
|
||||
github.com/saracen/walker v0.1.3
|
||||
golang.org/x/sys v0.14.0
|
||||
golang.org/x/term v0.13.0
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
|
45
go.sum
45
go.sum
@@ -1,48 +1,57 @@
|
||||
github.com/charlievieth/fastwalk v1.0.2 h1:KYWo7xszmoldOGrwdNIeznSzhj9mhgk+6DwHunG99bc=
|
||||
github.com/charlievieth/fastwalk v1.0.2/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
||||
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.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
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=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
36
install
36
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.44.0
|
||||
version=0.48.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -262,6 +262,12 @@ if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
else
|
||||
cat >> "$src" << EOF
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
$fzf_completion
|
||||
@@ -270,6 +276,7 @@ $fzf_completion
|
||||
# ------------
|
||||
$fzf_key_bindings
|
||||
EOF
|
||||
fi
|
||||
echo "OK"
|
||||
done
|
||||
|
||||
@@ -281,18 +288,6 @@ if [[ "$shells" =~ fish ]]; then
|
||||
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
|
||||
EOF
|
||||
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
||||
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
if [ $key_bindings -ne 0 ]; then
|
||||
echo -n "Symlink $fish_binding ... "
|
||||
ln -sf "$fzf_base/shell/key-bindings.fish" \
|
||||
"$fish_binding" && echo "OK" || echo "Failed"
|
||||
else
|
||||
echo -n "Removing $fish_binding ... "
|
||||
rm -f "$fish_binding"
|
||||
echo "OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
append_line() {
|
||||
@@ -355,12 +350,23 @@ done
|
||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf_key_bindings' \
|
||||
' fzf --fish | source' \
|
||||
'end'
|
||||
else
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
echo "Check $bind_file:"
|
||||
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $lno ]]; then
|
||||
echo " ** Found 'fzf_key_bindings' in line #$lno"
|
||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||
echo
|
||||
else
|
||||
echo " - Clear"
|
||||
echo
|
||||
append_line $update_config "fzf --fish | source" "$bind_file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.44.0"
|
||||
$version="0.48.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
45
main.go
45
main.go
@@ -1,14 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
fzf "github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.44"
|
||||
var version string = "0.48"
|
||||
var revision string = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
var bashKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.bash
|
||||
var bashCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.zsh
|
||||
var zshKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.zsh
|
||||
var zshCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
func printScript(label string, content []byte) {
|
||||
fmt.Println("### " + label + " ###")
|
||||
fmt.Println(strings.TrimSpace(string(content)))
|
||||
fmt.Println("### end: " + label + " ###")
|
||||
}
|
||||
|
||||
func main() {
|
||||
protector.Protect()
|
||||
fzf.Run(fzf.ParseOptions(), version, revision)
|
||||
options := fzf.ParseOptions()
|
||||
if options.Bash {
|
||||
printScript("key-bindings.bash", bashKeyBindings)
|
||||
printScript("completion.bash", bashCompletion)
|
||||
return
|
||||
}
|
||||
if options.Zsh {
|
||||
printScript("key-bindings.zsh", zshKeyBindings)
|
||||
printScript("completion.zsh", zshCompletion)
|
||||
return
|
||||
}
|
||||
if options.Fish {
|
||||
printScript("key-bindings.fish", fishKeyBindings)
|
||||
fmt.Println("fzf_key_bindings")
|
||||
return
|
||||
}
|
||||
fzf.Run(options, version, revision)
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Nov 2023" "fzf 0.44.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Mar 2024" "fzf 0.48.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
185
man/man1/fzf.1
185
man/man1/fzf.1
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Nov 2023" "fzf 0.44.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Mar 2024" "fzf 0.48.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -33,6 +33,10 @@ fzf [options]
|
||||
fzf is a general-purpose command-line fuzzy finder.
|
||||
|
||||
.SH OPTIONS
|
||||
.SS Note
|
||||
.TP
|
||||
Most long options have the opposite version with \fB--no-\fR prefix.
|
||||
|
||||
.SS Search mode
|
||||
.TP
|
||||
.B "-x, --extended"
|
||||
@@ -192,9 +196,21 @@ Label characters for \fBjump\fR and \fBjump-accept\fR
|
||||
.TP
|
||||
.BI "--height=" "[~]HEIGHT[%]"
|
||||
Display fzf window below the cursor with the given height instead of using
|
||||
the full screen. When prefixed with \fB~\fR, fzf will automatically determine
|
||||
the height in the range according to the input size. Note that adaptive height
|
||||
is not compatible with top/bottom margin and padding given in percent size.
|
||||
the full screen.
|
||||
|
||||
If a negative value is specified, the height is calculated as the terminal
|
||||
height minus the given value.
|
||||
|
||||
fzf --height=-1
|
||||
|
||||
When prefixed with \fB~\fR, fzf will automatically determine the height in the
|
||||
range according to the input size. Note that adaptive height is not compatible
|
||||
with top/bottom margin and padding given in percent size. It is also not
|
||||
compatible with a negative height value.
|
||||
|
||||
# Will not take up 100% of the screen
|
||||
seq 5 | fzf --height=~100%
|
||||
|
||||
.TP
|
||||
.BI "--min-height=" "HEIGHT"
|
||||
Minimum height when \fB--height\fR is given in percent (default: 10).
|
||||
@@ -248,9 +264,8 @@ Draw border around the finder
|
||||
.br
|
||||
|
||||
If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
|
||||
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
|
||||
\fB--no-unicode\fR.
|
||||
2 columns, try setting \fB--ambidouble\fR. If the border is still not properly
|
||||
rendered, set \fB--no-unicode\fR.
|
||||
|
||||
.TP
|
||||
.BI "--border-label" [=LABEL]
|
||||
@@ -301,6 +316,11 @@ the label. Label is printed on the top border line by default, add
|
||||
Use ASCII characters instead of Unicode drawing characters to draw borders,
|
||||
the spinner and the horizontal separator.
|
||||
|
||||
.TP
|
||||
.B "--ambidouble"
|
||||
Set this option if your terminal displays ambiguous width characters (e.g.
|
||||
box-drawing characters for borders) as 2 columns.
|
||||
|
||||
.TP
|
||||
.BI "--margin=" MARGIN
|
||||
Comma-separated expression for margins around the finder.
|
||||
@@ -562,10 +582,6 @@ e.g.
|
||||
When using a field index expression, leading and trailing whitespace is stripped
|
||||
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
|
||||
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
|
||||
all index numbers when multiple lines are selected.
|
||||
|
||||
A placeholder expression with \fBf\fR flag is replaced to the path of
|
||||
a temporary file that holds the evaluated list. This is useful when you
|
||||
multi-select a large number of items and the length of the evaluated string may
|
||||
@@ -577,6 +593,14 @@ e.g.
|
||||
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
|
||||
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
|
||||
|
||||
Also,
|
||||
|
||||
* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
|
||||
.br
|
||||
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
||||
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||
.br
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
|
||||
Preview window will be updated even when there is no match for the current
|
||||
@@ -600,7 +624,7 @@ The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-p
|
||||
script to render an image using either of the protocols inside the preview window.
|
||||
|
||||
e.g.
|
||||
\fBfzf --preview='fzf-preview.sh {}'
|
||||
\fBfzf --preview='fzf-preview.sh {}'\fR
|
||||
|
||||
.RE
|
||||
|
||||
@@ -811,12 +835,14 @@ e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
fzf --listen 6266
|
||||
|
||||
# Get program state in JSON format (experimental)
|
||||
curl localhost:6266
|
||||
|
||||
# Send action to the server
|
||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||
|
||||
# Get program state in JSON format (experimental)
|
||||
# * Make sure NOT to access this endpoint from execute/transform actions
|
||||
# as it will result in a timeout
|
||||
curl localhost:6266
|
||||
|
||||
# Start HTTP server on port 6266 with remote connections allowed
|
||||
# * Listening on non-localhost address requires using an API key
|
||||
export FZF_API_KEY="$(head -c 32 /dev/urandom | base64)"
|
||||
@@ -832,8 +858,49 @@ e.g.
|
||||
.B "--version"
|
||||
Display version information and exit
|
||||
|
||||
.SS Directory traversal
|
||||
.TP
|
||||
Note that most options have the opposite versions with \fB--no-\fR prefix.
|
||||
.B "--walker=[file][,dir][,follow][,hidden]"
|
||||
Determines the behavior of the built-in directory walker that is used when
|
||||
\fB$FZF_DEFAULT_COMMAND\fR is not set. The default value is \fBfile,follow,hidden\fR.
|
||||
|
||||
* \fBfile\fR: Include files in the search result
|
||||
.br
|
||||
* \fBdir\fR: Include directories in the search result
|
||||
.br
|
||||
* \fBhidden\fR: Include and follow hidden directories
|
||||
.br
|
||||
* \fBfollow\fR: Follow symbolic links
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "--walker-root=DIR"
|
||||
The root directory from which to start the built-in directory walker.
|
||||
The default value is the current working directory.
|
||||
|
||||
.TP
|
||||
.B "--walker-skip=DIRS"
|
||||
Comma-separated list of directory names to skip during the directory walk.
|
||||
The default value is \fB.git,node_modules\fR.
|
||||
|
||||
.SS Shell integration
|
||||
.TP
|
||||
.B "--bash"
|
||||
Print script to set up Bash shell integration
|
||||
|
||||
e.g. \fBeval "$(fzf --bash)"\fR
|
||||
|
||||
.TP
|
||||
.B "--zsh"
|
||||
Print script to set up Zsh shell integration
|
||||
|
||||
e.g. \fBeval "$(fzf --zsh)"\fR
|
||||
|
||||
.TP
|
||||
.B "--fish"
|
||||
Print script to set up Fish shell integration
|
||||
|
||||
e.g. \fBfzf --fish | source\fR
|
||||
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
@@ -843,7 +910,14 @@ with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in
|
||||
this case make sure that the command is POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
||||
Default options.
|
||||
.br
|
||||
e.g. \fBexport FZF_DEFAULT_OPTS="--layout=reverse --border --cycle"\fR
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS_FILE
|
||||
The location of the file that contains the default options.
|
||||
.br
|
||||
e.g. \fBexport FZF_DEFAULT_OPTS_FILE=~/.fzfrc\fR
|
||||
.TP
|
||||
.B FZF_API_KEY
|
||||
Can be used to require an API key when using \fB--listen\fR option. If not set,
|
||||
@@ -883,6 +957,39 @@ of field index expressions.
|
||||
.BR .. " All the fields"
|
||||
.br
|
||||
|
||||
.SH ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES
|
||||
|
||||
fzf exports the following environment variables to its child processes.
|
||||
|
||||
.BR FZF_LINES " Number of lines fzf takes up excluding padding and margin"
|
||||
.br
|
||||
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
|
||||
.br
|
||||
.BR FZF_TOTAL_COUNT " Total number of items"
|
||||
.br
|
||||
.BR FZF_MATCH_COUNT " Number of matched items"
|
||||
.br
|
||||
.BR FZF_SELECT_COUNT " Number of selected items"
|
||||
.br
|
||||
.BR FZF_QUERY " Current query string"
|
||||
.br
|
||||
.BR FZF_PROMPT " Prompt string"
|
||||
.br
|
||||
.BR FZF_ACTION " The name of the last action performed"
|
||||
.br
|
||||
.BR FZF_PORT " Port number when --listen option is used"
|
||||
.br
|
||||
|
||||
The following variables are additionally exported to the preview commands.
|
||||
|
||||
.BR FZF_PREVIEW_TOP " Top position of the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
|
||||
|
||||
.SH EXTENDED SEARCH MODE
|
||||
|
||||
Unless specified otherwise, fzf will start in "extended-search mode". In this
|
||||
@@ -1057,6 +1164,22 @@ e.g.
|
||||
\fB# Change the prompt to "loaded" when the input stream is complete
|
||||
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
|
||||
.RE
|
||||
\fIresize\fR
|
||||
.RS
|
||||
Triggered when the terminal size is changed.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
|
||||
.RE
|
||||
\fIresult\fR
|
||||
.RS
|
||||
Triggered when the filtering for the current query is complete and the result list is ready.
|
||||
|
||||
e.g.
|
||||
\fB# Put the cursor on the second item when the query string is empty
|
||||
# * Note that you can't use 'change' event in this case because the second position may not be available
|
||||
fzf --sync --bind 'result:transform:[[ -z {fzf:query} ]] && echo "pos(2)"'\fR
|
||||
.RE
|
||||
\fIchange\fR
|
||||
.RS
|
||||
Triggered whenever the query string is changed
|
||||
@@ -1120,6 +1243,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBaccept-or-print-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
|
||||
\fBbackward-char\fR \fIctrl-b left\fR
|
||||
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
@@ -1164,6 +1288,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBhide-header\fR
|
||||
\fBhide-preview\fR
|
||||
\fBoffset-down\fR (similar to CTRL-E of Vim)
|
||||
\fBoffset-up\fR (similar to CTRL-Y of Vim)
|
||||
@@ -1189,6 +1314,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBshow-header\fR
|
||||
\fBshow-preview\fR
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
@@ -1203,6 +1329,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||
@@ -1315,6 +1442,28 @@ e.g.
|
||||
\fB# You can still filter and select entries from the initial list for 3 seconds
|
||||
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
|
||||
|
||||
.SS TRANSFORM ACTIONS
|
||||
|
||||
Actions with \fBtransform-\fR prefix are used to transform the states of fzf
|
||||
using the output of an external command. The output of these commands are
|
||||
expected to be a single line of text.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind 'focus:transform-header:file --brief {}'\fR
|
||||
|
||||
\fBtransform(...)\fR action runs an external command that should print a series
|
||||
of actions to be performed. The output should be in the same format as the
|
||||
payload of HTTP POST request to the \fB--listen\fR server.
|
||||
|
||||
e.g.
|
||||
\fB# Disallow selecting an empty line
|
||||
echo -e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
|
||||
fzf --height '~100%' --reverse --header 'Select one' \\
|
||||
--bind 'enter:transform:[[ -n {} ]] &&
|
||||
echo accept ||
|
||||
echo "change-header:Invalid selection"'
|
||||
\fR
|
||||
|
||||
.SS PREVIEW BINDING
|
||||
|
||||
With \fBpreview(...)\fR action, you can specify multiple different preview
|
||||
|
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013-2023 Junegunn Choi
|
||||
" Copyright (c) 2013-2024 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
|
@@ -13,22 +13,19 @@
|
||||
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -F _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -F _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
@@ -63,6 +60,31 @@ __fzf_orig_completion() {
|
||||
done
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @var[out] REPLY - Original function name is returned
|
||||
__fzf_orig_completion_get_orig_func() {
|
||||
local cmd orig_var orig
|
||||
cmd=$1
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
REPLY="${orig##*#}"
|
||||
[[ $REPLY ]] && type "$REPLY" &> /dev/null
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @param $2 func - Fzf's completion function to replace the original function
|
||||
# @var[out] REPLY - Completion setting is returned as a string to "eval"
|
||||
__fzf_orig_completion_instantiate() {
|
||||
local cmd func orig_var orig
|
||||
cmd=$1
|
||||
func=$2
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
orig="${orig%#*}"
|
||||
[[ $orig == *' %s '* ]] || return 1
|
||||
printf -v REPLY "$orig" "$func"
|
||||
}
|
||||
|
||||
_fzf_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
@@ -261,15 +283,12 @@ _fzf_opts_completion() {
|
||||
}
|
||||
|
||||
_fzf_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret orig_cmd orig_complete
|
||||
local cmd ret REPLY orig_cmd orig_complete
|
||||
cmd="$1"
|
||||
shift
|
||||
orig_cmd="$1"
|
||||
orig_var="_fzf_orig_completion_$cmd"
|
||||
orig="${!orig_var-}"
|
||||
orig="${orig##*#}"
|
||||
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
@@ -277,6 +296,12 @@ _fzf_handle_dynamic_completion() {
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
|
||||
# Update orig_complete by _fzf_orig_completion entry
|
||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||
orig_complete=$REPLY
|
||||
|
||||
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
@@ -293,7 +318,6 @@ __fzf_generic_path_completion() {
|
||||
if [[ $cmd == \\* ]]; then
|
||||
cmd="${cmd:1}"
|
||||
fi
|
||||
cmd="${cmd//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
@@ -309,9 +333,18 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done)
|
||||
matches=$(
|
||||
unset FZF_DEFAULT_COMMAND
|
||||
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2"
|
||||
if declare -F "$1" > /dev/null; then
|
||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
|
||||
else
|
||||
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir"
|
||||
fi | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n "$matches" ]]; then
|
||||
@@ -359,8 +392,8 @@ _fzf_complete() {
|
||||
post="$(caller 0 | command awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
@@ -488,15 +521,12 @@ if type _completion_loader > /dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
__fzf_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
local cmd func opts REPLY
|
||||
cmd="$1"
|
||||
func="$2"
|
||||
opts="$3"
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
if [[ -n "$orig" ]]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
|
||||
eval "$REPLY"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
fi
|
||||
|
@@ -77,22 +77,19 @@ fi
|
||||
{
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
@@ -148,10 +145,19 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n "${(q)item} "
|
||||
done)
|
||||
matches=$(
|
||||
unset FZF_DEFAULT_COMMAND
|
||||
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}"
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
else
|
||||
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
|
||||
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty
|
||||
fi | while read item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
|
@@ -17,14 +17,9 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd opts
|
||||
cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | command cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
|
||||
eval "$cmd" |
|
||||
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
|
||||
local opts
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
printf '%q ' "$item" # escape special chars
|
||||
done
|
||||
@@ -42,11 +37,11 @@ fzf-file-widget() {
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd opts dir
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | command cut -b3-"}"
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
|
||||
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
|
||||
local opts dir
|
||||
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
|
||||
dir=$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)
|
||||
) && printf 'builtin cd -- %q' "$dir"
|
||||
}
|
||||
|
||||
if command -v perl > /dev/null; then
|
||||
|
@@ -25,18 +25,11 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# "-path \$dir'*/.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
@@ -81,13 +74,11 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd -- $result
|
||||
|
@@ -41,13 +41,9 @@ fi
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fsel() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local item
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
@@ -73,10 +69,8 @@ bindkey -M viins '^T' fzf-file-widget
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)"
|
||||
local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
@@ -98,13 +92,15 @@ bindkey -M viins '\ec' fzf-cd-widget
|
||||
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="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))"
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
if [ -n "$num" ]; then
|
||||
zle vi-fetch-history -n $num
|
||||
num=$(awk '{print $1}' <<< "$selected")
|
||||
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
|
||||
zle vi-fetch-history -n ${num%\*}
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
fi
|
||||
zle reset-prompt
|
||||
|
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2023 Junegunn Choi
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
129
src/actiontype_string.go
Normal file
129
src/actiontype_string.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
|
||||
|
||||
package fzf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[actIgnore-0]
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actChar-4]
|
||||
_ = x[actMouse-5]
|
||||
_ = x[actBeginningOfLine-6]
|
||||
_ = x[actAbort-7]
|
||||
_ = x[actAccept-8]
|
||||
_ = x[actAcceptNonEmpty-9]
|
||||
_ = x[actAcceptOrPrintQuery-10]
|
||||
_ = x[actBackwardChar-11]
|
||||
_ = x[actBackwardDeleteChar-12]
|
||||
_ = x[actBackwardDeleteCharEof-13]
|
||||
_ = x[actBackwardWord-14]
|
||||
_ = x[actCancel-15]
|
||||
_ = x[actChangeBorderLabel-16]
|
||||
_ = x[actChangeHeader-17]
|
||||
_ = x[actChangePreviewLabel-18]
|
||||
_ = x[actChangePrompt-19]
|
||||
_ = x[actChangeQuery-20]
|
||||
_ = x[actClearScreen-21]
|
||||
_ = x[actClearQuery-22]
|
||||
_ = x[actClearSelection-23]
|
||||
_ = x[actClose-24]
|
||||
_ = x[actDeleteChar-25]
|
||||
_ = x[actDeleteCharEof-26]
|
||||
_ = x[actEndOfLine-27]
|
||||
_ = x[actForwardChar-28]
|
||||
_ = x[actForwardWord-29]
|
||||
_ = x[actKillLine-30]
|
||||
_ = x[actKillWord-31]
|
||||
_ = x[actUnixLineDiscard-32]
|
||||
_ = x[actUnixWordRubout-33]
|
||||
_ = x[actYank-34]
|
||||
_ = x[actBackwardKillWord-35]
|
||||
_ = x[actSelectAll-36]
|
||||
_ = x[actDeselectAll-37]
|
||||
_ = x[actToggle-38]
|
||||
_ = x[actToggleSearch-39]
|
||||
_ = x[actToggleAll-40]
|
||||
_ = x[actToggleDown-41]
|
||||
_ = x[actToggleUp-42]
|
||||
_ = x[actToggleIn-43]
|
||||
_ = x[actToggleOut-44]
|
||||
_ = x[actToggleTrack-45]
|
||||
_ = x[actToggleHeader-46]
|
||||
_ = x[actTrack-47]
|
||||
_ = x[actDown-48]
|
||||
_ = x[actUp-49]
|
||||
_ = x[actPageUp-50]
|
||||
_ = x[actPageDown-51]
|
||||
_ = x[actPosition-52]
|
||||
_ = x[actHalfPageUp-53]
|
||||
_ = x[actHalfPageDown-54]
|
||||
_ = x[actOffsetUp-55]
|
||||
_ = x[actOffsetDown-56]
|
||||
_ = x[actJump-57]
|
||||
_ = x[actJumpAccept-58]
|
||||
_ = x[actPrintQuery-59]
|
||||
_ = x[actRefreshPreview-60]
|
||||
_ = x[actReplaceQuery-61]
|
||||
_ = x[actToggleSort-62]
|
||||
_ = x[actShowPreview-63]
|
||||
_ = x[actHidePreview-64]
|
||||
_ = x[actTogglePreview-65]
|
||||
_ = x[actTogglePreviewWrap-66]
|
||||
_ = x[actTransform-67]
|
||||
_ = x[actTransformBorderLabel-68]
|
||||
_ = x[actTransformHeader-69]
|
||||
_ = x[actTransformPreviewLabel-70]
|
||||
_ = x[actTransformPrompt-71]
|
||||
_ = x[actTransformQuery-72]
|
||||
_ = x[actPreview-73]
|
||||
_ = x[actChangePreview-74]
|
||||
_ = x[actChangePreviewWindow-75]
|
||||
_ = x[actPreviewTop-76]
|
||||
_ = x[actPreviewBottom-77]
|
||||
_ = x[actPreviewUp-78]
|
||||
_ = x[actPreviewDown-79]
|
||||
_ = x[actPreviewPageUp-80]
|
||||
_ = x[actPreviewPageDown-81]
|
||||
_ = x[actPreviewHalfPageUp-82]
|
||||
_ = x[actPreviewHalfPageDown-83]
|
||||
_ = x[actPrevHistory-84]
|
||||
_ = x[actPrevSelected-85]
|
||||
_ = x[actPut-86]
|
||||
_ = x[actNextHistory-87]
|
||||
_ = x[actNextSelected-88]
|
||||
_ = x[actExecute-89]
|
||||
_ = x[actExecuteSilent-90]
|
||||
_ = x[actExecuteMulti-91]
|
||||
_ = x[actSigStop-92]
|
||||
_ = x[actFirst-93]
|
||||
_ = x[actLast-94]
|
||||
_ = x[actReload-95]
|
||||
_ = x[actReloadSync-96]
|
||||
_ = x[actDisableSearch-97]
|
||||
_ = x[actEnableSearch-98]
|
||||
_ = x[actSelect-99]
|
||||
_ = x[actDeselect-100]
|
||||
_ = x[actUnbind-101]
|
||||
_ = x[actRebind-102]
|
||||
_ = x[actBecome-103]
|
||||
_ = x[actResponse-104]
|
||||
_ = x[actShowHeader-105]
|
||||
_ = x[actHideHeader-106]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
|
||||
}
|
@@ -255,24 +255,29 @@ func charClassOf(char rune) charClass {
|
||||
|
||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||
if class > charNonWord {
|
||||
if prevClass == charWhite {
|
||||
switch prevClass {
|
||||
case charWhite:
|
||||
// Word boundary after whitespace
|
||||
return bonusBoundaryWhite
|
||||
} else if prevClass == charDelimiter {
|
||||
case charDelimiter:
|
||||
// Word boundary after a delimiter character
|
||||
return bonusBoundaryDelimiter
|
||||
} else if prevClass == charNonWord {
|
||||
case charNonWord:
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
}
|
||||
}
|
||||
|
||||
if prevClass == charLower && class == charUpper ||
|
||||
prevClass != charNumber && class == charNumber {
|
||||
// camelCase letter123
|
||||
return bonusCamel123
|
||||
} else if class == charNonWord {
|
||||
}
|
||||
|
||||
switch class {
|
||||
case charNonWord, charDelimiter:
|
||||
return bonusNonWord
|
||||
} else if class == charWhite {
|
||||
case charWhite:
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return 0
|
||||
|
@@ -351,9 +351,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
ptr := &state.fg
|
||||
|
||||
var delimiter byte = 0
|
||||
count := 0
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
|
||||
count++
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -435,6 +437,13 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
@@ -348,6 +348,9 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
|
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -54,16 +53,6 @@ const (
|
||||
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
|
||||
)
|
||||
|
||||
var defaultCommand string
|
||||
|
||||
func init() {
|
||||
if !util.IsWindows() {
|
||||
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
|
||||
} else if os.Getenv("TERM") == "cygwin" {
|
||||
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
|
||||
}
|
||||
}
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
|
19
src/core.go
19
src/core.go
@@ -117,7 +117,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource()
|
||||
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
}
|
||||
|
||||
// Matcher
|
||||
@@ -165,7 +165,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero, false)
|
||||
reader.ReadSource()
|
||||
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
@@ -200,7 +200,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad(opts)
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
go terminal.Loop()
|
||||
@@ -213,6 +213,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
reading := true
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
total := 0
|
||||
query := []rune{}
|
||||
@@ -232,13 +233,13 @@ func Run(opts *Options, version string, revision string) {
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command string) {
|
||||
restart := func(command string, environ []string) {
|
||||
reading = true
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision++
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
go reader.restart(command, environ)
|
||||
}
|
||||
for {
|
||||
delay := true
|
||||
@@ -266,8 +267,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
os.Exit(value.(int))
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
restart(*nextCommand, nextEnviron)
|
||||
nextCommand = nil
|
||||
nextEnviron = nil
|
||||
break
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
@@ -292,11 +294,13 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var environ []string
|
||||
var changed bool
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
@@ -306,8 +310,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
nextCommand = command
|
||||
nextEnviron = environ
|
||||
} else {
|
||||
restart(*command)
|
||||
restart(*command, environ)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
|
206
src/options.go
206
src/options.go
@@ -12,8 +12,8 @@ import (
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
const usage = `usage: fzf [options]
|
||||
@@ -57,6 +57,8 @@ const usage = `usage: fzf [options]
|
||||
Layout
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calcalated 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
|
||||
@@ -122,10 +124,21 @@ const usage = `usage: fzf [options]
|
||||
(To allow remote process execution, use --listen-unsafe)
|
||||
--version Display version information and exit
|
||||
|
||||
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
|
||||
|
||||
Environment variables
|
||||
FZF_DEFAULT_COMMAND Default command to use when input is tty
|
||||
FZF_DEFAULT_OPTS Default options
|
||||
(e.g. '--layout=reverse --inline-info')
|
||||
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)
|
||||
|
||||
`
|
||||
@@ -157,6 +170,7 @@ type heightSpec struct {
|
||||
size float64
|
||||
percent bool
|
||||
auto bool
|
||||
inverse bool
|
||||
}
|
||||
|
||||
type sizeSpec struct {
|
||||
@@ -271,8 +285,18 @@ func firstLine(s string) string {
|
||||
return strings.SplitN(s, "\n", 2)[0]
|
||||
}
|
||||
|
||||
type walkerOpts struct {
|
||||
file bool
|
||||
dir bool
|
||||
hidden bool
|
||||
follow bool
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
type Options struct {
|
||||
Bash bool
|
||||
Zsh bool
|
||||
Fish bool
|
||||
Fuzzy bool
|
||||
FuzzyAlgo algo.Algo
|
||||
Scheme string
|
||||
@@ -334,19 +358,36 @@ type Options struct {
|
||||
BorderLabel labelOpts
|
||||
PreviewLabel labelOpts
|
||||
Unicode bool
|
||||
Ambidouble bool
|
||||
Tabstop int
|
||||
ListenAddr *listenAddress
|
||||
Unsafe bool
|
||||
ClearOnExit bool
|
||||
WalkerOpts walkerOpts
|
||||
WalkerRoot string
|
||||
WalkerSkip []string
|
||||
Version bool
|
||||
}
|
||||
|
||||
func filterNonEmpty(input []string) []string {
|
||||
output := make([]string, 0, len(input))
|
||||
for _, str := range input {
|
||||
if len(str) > 0 {
|
||||
output = append(output, str)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
return &Options{
|
||||
Bash: false,
|
||||
Zsh: false,
|
||||
Fish: false,
|
||||
Fuzzy: true,
|
||||
FuzzyAlgo: algo.FuzzyMatchV2,
|
||||
Scheme: "default",
|
||||
@@ -403,11 +444,15 @@ func defaultOptions() *Options {
|
||||
Margin: defaultMargin(),
|
||||
Padding: defaultMargin(),
|
||||
Unicode: true,
|
||||
Ambidouble: os.Getenv("RUNEWIDTH_EASTASIAN") == "1",
|
||||
Tabstop: 8,
|
||||
BorderLabel: labelOpts{},
|
||||
PreviewLabel: labelOpts{},
|
||||
Unsafe: false,
|
||||
ClearOnExit: true,
|
||||
WalkerOpts: walkerOpts{file: true, hidden: true, follow: true},
|
||||
WalkerRoot: ".",
|
||||
WalkerSkip: []string{".git", "node_modules"},
|
||||
Version: false}
|
||||
}
|
||||
|
||||
@@ -416,8 +461,10 @@ func help(code int) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
var errorContext = ""
|
||||
|
||||
func errorExit(msg string) {
|
||||
os.Stderr.WriteString(msg + "\n")
|
||||
os.Stderr.WriteString(errorContext + msg + "\n")
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
@@ -647,6 +694,10 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.Load)
|
||||
case "focus":
|
||||
add(tui.Focus)
|
||||
case "result":
|
||||
add(tui.Result)
|
||||
case "resize":
|
||||
add(tui.Resize)
|
||||
case "one":
|
||||
add(tui.One)
|
||||
case "zero":
|
||||
@@ -955,6 +1006,30 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
||||
return theme
|
||||
}
|
||||
|
||||
func parseWalkerOpts(str string) walkerOpts {
|
||||
opts := walkerOpts{}
|
||||
for _, str := range strings.Split(strings.ToLower(str), ",") {
|
||||
switch str {
|
||||
case "file":
|
||||
opts.file = true
|
||||
case "dir":
|
||||
opts.dir = true
|
||||
case "hidden":
|
||||
opts.hidden = true
|
||||
case "follow":
|
||||
opts.follow = true
|
||||
case "":
|
||||
// Ignored
|
||||
default:
|
||||
errorExit("invalid walker option: " + str)
|
||||
}
|
||||
}
|
||||
if !opts.file && !opts.dir {
|
||||
errorExit("at least one of 'file' or 'dir' should be specified")
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
var (
|
||||
executeRegexp *regexp.Regexp
|
||||
splitRegexp *regexp.Regexp
|
||||
@@ -976,7 +1051,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?: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|change-preview|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1070,6 +1145,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actAccept)
|
||||
case "accept-non-empty":
|
||||
appendAction(actAcceptNonEmpty)
|
||||
case "accept-or-print-query":
|
||||
appendAction(actAcceptOrPrintQuery)
|
||||
case "print-query":
|
||||
appendAction(actPrintQuery)
|
||||
case "refresh-preview":
|
||||
@@ -1081,7 +1158,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
case "backward-delete-char":
|
||||
appendAction(actBackwardDeleteChar)
|
||||
case "backward-delete-char/eof":
|
||||
appendAction(actBackwardDeleteCharEOF)
|
||||
appendAction(actBackwardDeleteCharEof)
|
||||
case "backward-word":
|
||||
appendAction(actBackwardWord)
|
||||
case "clear-screen":
|
||||
@@ -1089,7 +1166,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
case "delete-char":
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
appendAction(actDeleteCharEof)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
@@ -1136,6 +1213,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleTrack)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "show-header":
|
||||
appendAction(actShowHeader)
|
||||
case "hide-header":
|
||||
appendAction(actHideHeader)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "select":
|
||||
@@ -1208,7 +1289,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actDisableSearch)
|
||||
case "put":
|
||||
if putAllowed {
|
||||
appendAction(actRune)
|
||||
appendAction(actChar)
|
||||
} else {
|
||||
exit("unable to put non-printable character")
|
||||
}
|
||||
@@ -1328,6 +1409,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actExecuteMulti
|
||||
case "put":
|
||||
return actPut
|
||||
case "transform":
|
||||
return actTransform
|
||||
case "transform-border-label":
|
||||
return actTransformBorderLabel
|
||||
case "transform-preview-label":
|
||||
@@ -1384,6 +1467,13 @@ func parseHeight(str string) heightSpec {
|
||||
heightSpec.auto = true
|
||||
str = str[1:]
|
||||
}
|
||||
if strings.HasPrefix(str, "-") {
|
||||
if heightSpec.auto {
|
||||
errorExit("negative(-) height is not compatible with adaptive(~) height")
|
||||
}
|
||||
heightSpec.inverse = true
|
||||
str = str[1:]
|
||||
}
|
||||
|
||||
size := parseSize(str, 100, "height")
|
||||
heightSpec.size = size.size
|
||||
@@ -1573,11 +1663,24 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
}
|
||||
}
|
||||
validateJumpLabels := false
|
||||
validatePointer := false
|
||||
validateMarker := false
|
||||
for i := 0; i < len(allArgs); i++ {
|
||||
arg := allArgs[i]
|
||||
switch arg {
|
||||
case "--bash":
|
||||
opts.Bash = true
|
||||
if opts.Zsh || opts.Fish {
|
||||
errorExit("cannot specify --bash with --zsh or --fish")
|
||||
}
|
||||
case "--zsh":
|
||||
opts.Zsh = true
|
||||
if opts.Bash || opts.Fish {
|
||||
errorExit("cannot specify --zsh with --bash or --fish")
|
||||
}
|
||||
case "--fish":
|
||||
opts.Fish = true
|
||||
if opts.Bash || opts.Zsh {
|
||||
errorExit("cannot specify --fish with --bash or --zsh")
|
||||
}
|
||||
case "-h", "--help":
|
||||
help(exitOk)
|
||||
case "-x", "--extended":
|
||||
@@ -1754,10 +1857,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Prompt = nextString(allArgs, &i, "prompt string required")
|
||||
case "--pointer":
|
||||
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
|
||||
validatePointer = true
|
||||
case "--marker":
|
||||
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
|
||||
validateMarker = true
|
||||
case "--sync":
|
||||
opts.Sync = true
|
||||
case "--no-sync":
|
||||
@@ -1825,6 +1926,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Unicode = false
|
||||
case "--unicode":
|
||||
opts.Unicode = true
|
||||
case "--ambidouble":
|
||||
opts.Ambidouble = true
|
||||
case "--no-ambidouble":
|
||||
opts.Ambidouble = false
|
||||
case "--margin":
|
||||
opts.Margin = parseMargin(
|
||||
"margin",
|
||||
@@ -1840,7 +1945,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
addr := defaultListenAddr
|
||||
if given {
|
||||
var err error
|
||||
err, addr = parseListenAddress(str)
|
||||
addr, err = parseListenAddress(str)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
@@ -1854,6 +1959,12 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.ClearOnExit = true
|
||||
case "--no-clear":
|
||||
opts.ClearOnExit = false
|
||||
case "--walker":
|
||||
opts.WalkerOpts = parseWalkerOpts(nextString(allArgs, &i, "walker options required [file][,dir][,follow][,hidden]"))
|
||||
case "--walker-root":
|
||||
opts.WalkerRoot = nextString(allArgs, &i, "directory required")
|
||||
case "--walker-skip":
|
||||
opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ","))
|
||||
case "--version":
|
||||
opts.Version = true
|
||||
case "--":
|
||||
@@ -1883,10 +1994,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Prompt = value
|
||||
} else if match, value := optString(arg, "--pointer="); match {
|
||||
opts.Pointer = firstLine(value)
|
||||
validatePointer = true
|
||||
} else if match, value := optString(arg, "--marker="); match {
|
||||
opts.Marker = firstLine(value)
|
||||
validateMarker = true
|
||||
} else if match, value := optString(arg, "-n", "--nth="); match {
|
||||
opts.Nth = splitNth(value)
|
||||
} else if match, value := optString(arg, "--with-nth="); match {
|
||||
@@ -1940,19 +2049,25 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
err, addr := parseListenAddress(value)
|
||||
addr, err := parseListenAddress(value)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
opts.ListenAddr = &addr
|
||||
opts.Unsafe = false
|
||||
} else if match, value := optString(arg, "--listen-unsafe="); match {
|
||||
err, addr := parseListenAddress(value)
|
||||
addr, err := parseListenAddress(value)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
opts.ListenAddr = &addr
|
||||
opts.Unsafe = true
|
||||
} else if match, value := optString(arg, "--walker="); match {
|
||||
opts.WalkerOpts = parseWalkerOpts(value)
|
||||
} else if match, value := optString(arg, "--walker-root="); match {
|
||||
opts.WalkerRoot = value
|
||||
} else if match, value := optString(arg, "--walker-skip="); match {
|
||||
opts.WalkerSkip = filterNonEmpty(strings.Split(value, ","))
|
||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||
opts.HscrollOff = atoi(value)
|
||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||
@@ -1993,31 +2108,31 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if validatePointer {
|
||||
if err := validateSign(opts.Pointer, "pointer"); err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if validateMarker {
|
||||
if err := validateSign(opts.Marker, "marker"); err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateSign(sign string, signOptName string) error {
|
||||
if sign == "" {
|
||||
return fmt.Errorf("%v cannot be empty", signOptName)
|
||||
}
|
||||
if runewidth.StringWidth(sign) > 2 {
|
||||
if uniseg.StringWidth(sign) > 2 {
|
||||
return fmt.Errorf("%v display width should be up to 2", signOptName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postProcessOptions(opts *Options) {
|
||||
if opts.Ambidouble {
|
||||
uniseg.EastAsianAmbiguousWidth = 2
|
||||
}
|
||||
|
||||
if err := validateSign(opts.Pointer, "pointer"); err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
|
||||
if err := validateSign(opts.Marker, "marker"); err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
|
||||
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
|
||||
errorExit("--height option is currently not supported on this platform")
|
||||
}
|
||||
@@ -2028,7 +2143,7 @@ func postProcessOptions(opts *Options) {
|
||||
errorExit("--scrollbar should be given one or two characters")
|
||||
}
|
||||
for _, r := range runes {
|
||||
if runewidth.RuneWidth(r) != 1 {
|
||||
if uniseg.StringWidth(string(r)) != 1 {
|
||||
errorExit("scrollbar display width should be 1")
|
||||
}
|
||||
}
|
||||
@@ -2145,13 +2260,36 @@ func ParseOptions() *Options {
|
||||
}
|
||||
}
|
||||
|
||||
// Options from Env var
|
||||
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
// 1. Options from $FZF_DEFAULT_OPTS_FILE
|
||||
if path := os.Getenv("FZF_DEFAULT_OPTS_FILE"); path != "" {
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
errorContext = "$FZF_DEFAULT_OPTS_FILE: "
|
||||
errorExit(err.Error())
|
||||
}
|
||||
|
||||
words, parseErr := shellwords.Parse(string(bytes))
|
||||
if parseErr != nil {
|
||||
errorContext = path + ": "
|
||||
errorExit(parseErr.Error())
|
||||
}
|
||||
if len(words) > 0 {
|
||||
parseOptions(opts, words)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Options from $FZF_DEFAULT_OPTS string
|
||||
words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
errorContext = "$FZF_DEFAULT_OPTS: "
|
||||
if parseErr != nil {
|
||||
errorExit(parseErr.Error())
|
||||
}
|
||||
if len(words) > 0 {
|
||||
parseOptions(opts, words)
|
||||
}
|
||||
|
||||
// Options from command-line arguments
|
||||
// 3. Options from command-line arguments
|
||||
errorContext = ""
|
||||
parseOptions(opts, os.Args[1:])
|
||||
|
||||
postProcessOptions(opts)
|
||||
|
@@ -209,11 +209,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
text = text[1:]
|
||||
} else {
|
||||
typ = termFuzzy
|
||||
text = text[1:]
|
||||
}
|
||||
text = text[1:]
|
||||
} else if strings.HasPrefix(text, "^") {
|
||||
if typ == termSuffix {
|
||||
typ = termEqual
|
||||
|
@@ -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")
|
||||
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
@@ -6,14 +6,13 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/charlievieth/fastwalk"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// Reader reads from command or standard input
|
||||
@@ -77,48 +76,33 @@ func (r *Reader) fin(success bool) {
|
||||
|
||||
func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
defer func() { r.mutex.Unlock() }()
|
||||
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
util.KillCommand(r.exec)
|
||||
} else if defaultCommand != "" {
|
||||
} else {
|
||||
os.Stdin.Close()
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command string) {
|
||||
func (r *Reader) restart(command string, environ []string) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(nil, command)
|
||||
success := r.readFromCommand(command, environ)
|
||||
r.fin(success)
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
// The default command for *nix requires a shell that supports "pipefail"
|
||||
// https://unix.stackexchange.com/a/654932/62171
|
||||
shell := "bash"
|
||||
currentShell := os.Getenv("SHELL")
|
||||
currentShellName := path.Base(currentShell)
|
||||
for _, shellName := range []string{"bash", "zsh", "ksh", "ash", "hush", "mksh", "yash"} {
|
||||
if currentShellName == shellName {
|
||||
shell = currentShell
|
||||
break
|
||||
}
|
||||
}
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
if defaultCommand != "" {
|
||||
success = r.readFromCommand(&shell, defaultCommand)
|
||||
} else {
|
||||
success = r.readFiles()
|
||||
}
|
||||
success = r.readFiles(root, opts, ignores)
|
||||
} else {
|
||||
success = r.readFromCommand(nil, cmd)
|
||||
// We can't export FZF_* environment variables to the default command
|
||||
success = r.readFromCommand(cmd, nil)
|
||||
}
|
||||
} else {
|
||||
success = r.readFromStdin()
|
||||
@@ -161,16 +145,28 @@ func (r *Reader) readFromStdin() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles() bool {
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
r.killed = false
|
||||
fn := func(path string, mode os.FileInfo) error {
|
||||
conf := fastwalk.Config{Follow: opts.follow}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
if path != "." {
|
||||
isDir := mode.Mode().IsDir()
|
||||
if isDir && filepath.Base(path)[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
isDir := de.IsDir()
|
||||
if isDir {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if ignore == base {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isDir && r.pusher([]byte(path)) {
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
@@ -181,20 +177,16 @@ func (r *Reader) readFiles() bool {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
cb := walker.WithErrorCallback(func(pathname string, err error) error {
|
||||
return nil
|
||||
})
|
||||
return walker.Walk(".", fn, cb) == nil
|
||||
return fastwalk.Walk(&conf, root, fn) == nil
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(shell *string, command string) bool {
|
||||
func (r *Reader) readFromCommand(command string, environ []string) bool {
|
||||
r.mutex.Lock()
|
||||
r.killed = false
|
||||
r.command = &command
|
||||
if shell != nil {
|
||||
r.exec = util.ExecCommandWith(*shell, command, true)
|
||||
} else {
|
||||
r.exec = util.ExecCommand(command, true)
|
||||
r.exec = util.ExecCommand(command, true)
|
||||
if environ != nil {
|
||||
r.exec.Env = environ
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
|
@@ -22,7 +22,7 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func TestReadFromCommand(t *testing.T) {
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(nil, `no-such-command`))
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
t.Errorf("%s", strs)
|
||||
|
@@ -80,7 +80,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()))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,9 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
// Negative of 1-based index of itemColors
|
||||
// - The extra -1 means highlighted
|
||||
cols[i] = cols[i]*-1 - 1
|
||||
if cols[i] >= 0 {
|
||||
cols[i] = cols[i]*-1 - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -120,7 +120,7 @@ func TestColorOffset(t *testing.T) {
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
|
||||
offsets := []Offset{{5, 15}, {25, 35}}
|
||||
offsets := []Offset{{5, 15}, {10, 12}, {25, 35}}
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
|
@@ -30,7 +30,9 @@ const (
|
||||
httpOk = "HTTP/1.1 200 OK" + crlf
|
||||
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
||||
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
|
||||
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
|
||||
httpReadTimeout = 10 * time.Second
|
||||
jsonContentType = "Content-Type: application/json" + crlf
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
@@ -51,47 +53,47 @@ func (addr listenAddress) IsLocal() bool {
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
||||
|
||||
func parseListenAddress(address string) (error, listenAddress) {
|
||||
func parseListenAddress(address string) (listenAddress, error) {
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
|
||||
}
|
||||
portStr := parts[len(parts)-1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return nil, listenAddress{parts[0], port}
|
||||
return listenAddress{parts[0], port}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
|
||||
return port, fmt.Errorf("FZF_API_KEY is required to allow remote access")
|
||||
}
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err := net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s", addrStr), port
|
||||
return port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||
return port, fmt.Errorf("cannot extract port: %s", addr)
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return err, port
|
||||
return port, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
|
||||
listener.Close()
|
||||
}()
|
||||
|
||||
return nil, port
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
@@ -141,7 +143,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
return answer(httpBadRequest, message)
|
||||
}
|
||||
good := func(message string) string {
|
||||
return answer(httpOk+"Content-Type: application/json"+crlf, message)
|
||||
return answer(httpOk+jsonContentType, message)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
@@ -165,8 +167,16 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
||||
response := <-server.responseChannel
|
||||
return good(response)
|
||||
select {
|
||||
case response := <-server.responseChannel:
|
||||
return good(response)
|
||||
case <-time.After(2 * time.Second):
|
||||
go func() {
|
||||
// Drain the channel
|
||||
<-server.responseChannel
|
||||
}()
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
}
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
@@ -218,7 +228,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
}
|
||||
|
||||
server.actionChannel <- actions
|
||||
return httpOk
|
||||
return httpOk + crlf
|
||||
}
|
||||
|
||||
func parseGetParams(query string) getParams {
|
||||
@@ -227,15 +237,13 @@ func parseGetParams(query string) getParams {
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
switch parts[0] {
|
||||
case "limit":
|
||||
val, err := strconv.Atoi(parts[1])
|
||||
if err == nil {
|
||||
params.limit = val
|
||||
}
|
||||
case "offset":
|
||||
val, err := strconv.Atoi(parts[1])
|
||||
if err == nil {
|
||||
params.offset = val
|
||||
case "limit", "offset":
|
||||
if val, err := strconv.Atoi(parts[1]); err == nil {
|
||||
if parts[0] == "limit" {
|
||||
params.limit = val
|
||||
} else {
|
||||
params.offset = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
327
src/terminal.go
327
src/terminal.go
@@ -17,7 +17,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
@@ -52,11 +51,12 @@ var offsetComponentRegex *regexp.Regexp
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var activeTempFiles []string
|
||||
var passThroughRegex *regexp.Regexp
|
||||
var actionTypeRegex *regexp.Regexp
|
||||
|
||||
const clearCode string = "\x1b[2J"
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||
@@ -67,7 +67,7 @@ func init() {
|
||||
// * https://sw.kovidgoyal.net/kitty/graphics-protocol
|
||||
// * https://en.wikipedia.org/wiki/Sixel
|
||||
// * https://iterm2.com/documentation-images.html
|
||||
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?\a`)
|
||||
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\\r?|\x1b]1337;.*?(\a|\x1b\\)`)
|
||||
}
|
||||
|
||||
type jumpMode int
|
||||
@@ -182,6 +182,7 @@ type Terminal struct {
|
||||
separator labelPrinter
|
||||
separatorLen int
|
||||
spinner []string
|
||||
promptString string
|
||||
prompt func()
|
||||
promptLen int
|
||||
borderLabel labelPrinter
|
||||
@@ -249,7 +250,10 @@ type Terminal struct {
|
||||
borderWidth int
|
||||
count int
|
||||
progress int
|
||||
hasResultActions bool
|
||||
hasFocusActions bool
|
||||
hasLoadActions bool
|
||||
hasResizeActions bool
|
||||
triggerLoad bool
|
||||
reading bool
|
||||
running bool
|
||||
@@ -285,6 +289,11 @@ type Terminal struct {
|
||||
tui tui.Renderer
|
||||
executing *util.AtomicBool
|
||||
termSize tui.TermSize
|
||||
lastAction actionType
|
||||
lastFocus int32
|
||||
areaLines int
|
||||
areaColumns int
|
||||
forcePreview bool
|
||||
}
|
||||
|
||||
type selectedItem struct {
|
||||
@@ -332,20 +341,24 @@ type action struct {
|
||||
a string
|
||||
}
|
||||
|
||||
//go:generate stringer -type=actionType
|
||||
type actionType int
|
||||
|
||||
const (
|
||||
actIgnore actionType = iota
|
||||
actStart
|
||||
actClick
|
||||
actInvalid
|
||||
actRune
|
||||
actChar
|
||||
actMouse
|
||||
actBeginningOfLine
|
||||
actAbort
|
||||
actAccept
|
||||
actAcceptNonEmpty
|
||||
actAcceptOrPrintQuery
|
||||
actBackwardChar
|
||||
actBackwardDeleteChar
|
||||
actBackwardDeleteCharEOF
|
||||
actBackwardDeleteCharEof
|
||||
actBackwardWord
|
||||
actCancel
|
||||
actChangeBorderLabel
|
||||
@@ -358,7 +371,7 @@ const (
|
||||
actClearSelection
|
||||
actClose
|
||||
actDeleteChar
|
||||
actDeleteCharEOF
|
||||
actDeleteCharEof
|
||||
actEndOfLine
|
||||
actForwardChar
|
||||
actForwardWord
|
||||
@@ -399,6 +412,7 @@ const (
|
||||
actHidePreview
|
||||
actTogglePreview
|
||||
actTogglePreviewWrap
|
||||
actTransform
|
||||
actTransformBorderLabel
|
||||
actTransformHeader
|
||||
actTransformPreviewLabel
|
||||
@@ -436,17 +450,32 @@ const (
|
||||
actRebind
|
||||
actBecome
|
||||
actResponse
|
||||
actShowHeader
|
||||
actHideHeader
|
||||
)
|
||||
|
||||
func (a actionType) Name() string {
|
||||
name := ""
|
||||
for i, r := range a.String()[3:] {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
name += "-"
|
||||
}
|
||||
name += string(r)
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
func processExecution(action actionType) bool {
|
||||
switch action {
|
||||
case actTransformBorderLabel,
|
||||
case actTransform,
|
||||
actTransformBorderLabel,
|
||||
actTransformHeader,
|
||||
actTransformPreviewLabel,
|
||||
actTransformPrompt,
|
||||
actTransformQuery,
|
||||
actPreview,
|
||||
actChangePreview,
|
||||
actRefreshPreview,
|
||||
actExecute,
|
||||
actExecuteSilent,
|
||||
actExecuteMulti,
|
||||
@@ -462,7 +491,7 @@ type placeholderFlags struct {
|
||||
plus bool
|
||||
preserveSpace bool
|
||||
number bool
|
||||
query bool
|
||||
forceUpdate bool
|
||||
file bool
|
||||
}
|
||||
|
||||
@@ -470,11 +499,13 @@ type searchRequest struct {
|
||||
sort bool
|
||||
sync bool
|
||||
command *string
|
||||
environ []string
|
||||
changed bool
|
||||
}
|
||||
|
||||
type previewRequest struct {
|
||||
template string
|
||||
pwindow tui.Window
|
||||
pwindowSize tui.TermSize
|
||||
scrollOffset int
|
||||
list []*Item
|
||||
@@ -505,14 +536,13 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
}
|
||||
|
||||
add(tui.Invalid, actInvalid)
|
||||
add(tui.Resize, actClearScreen)
|
||||
add(tui.CtrlA, actBeginningOfLine)
|
||||
add(tui.CtrlB, actBackwardChar)
|
||||
add(tui.CtrlC, actAbort)
|
||||
add(tui.CtrlG, actAbort)
|
||||
add(tui.CtrlQ, actAbort)
|
||||
add(tui.ESC, actAbort)
|
||||
add(tui.CtrlD, actDeleteCharEOF)
|
||||
add(tui.CtrlD, actDeleteCharEof)
|
||||
add(tui.CtrlE, actEndOfLine)
|
||||
add(tui.CtrlF, actForwardChar)
|
||||
add(tui.CtrlH, actBackwardDeleteChar)
|
||||
@@ -554,7 +584,7 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
add(tui.SDown, actPreviewDown)
|
||||
|
||||
add(tui.Mouse, actMouse)
|
||||
add(tui.LeftClick, actIgnore)
|
||||
add(tui.LeftClick, actClick)
|
||||
add(tui.RightClick, actToggle)
|
||||
add(tui.SLeftClick, actToggle)
|
||||
add(tui.SRightClick, actToggle)
|
||||
@@ -573,10 +603,14 @@ func trimQuery(query string) []rune {
|
||||
return []rune(strings.Replace(query, "\t", " ", -1))
|
||||
}
|
||||
|
||||
func hasPreviewAction(opts *Options) bool {
|
||||
func mayTriggerPreview(opts *Options) bool {
|
||||
if opts.ListenAddr != nil {
|
||||
return true
|
||||
}
|
||||
for _, actions := range opts.Keymap {
|
||||
for _, action := range actions {
|
||||
if action.t == actPreview || action.t == actChangePreview {
|
||||
switch action.t {
|
||||
case actPreview, actChangePreview, actTransform:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -592,10 +626,17 @@ func makeSpinner(unicode bool) []string {
|
||||
}
|
||||
|
||||
func evaluateHeight(opts *Options, termHeight int) int {
|
||||
size := opts.Height.size
|
||||
if opts.Height.percent {
|
||||
return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
|
||||
if opts.Height.inverse {
|
||||
size = 100 - size
|
||||
}
|
||||
return util.Max(int(size*float64(termHeight)/100.0), opts.MinHeight)
|
||||
}
|
||||
return int(opts.Height.size)
|
||||
if opts.Height.inverse {
|
||||
size = float64(termHeight) - size
|
||||
}
|
||||
return int(size)
|
||||
}
|
||||
|
||||
// NewTerminal returns new Terminal object
|
||||
@@ -608,8 +649,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
delay = initialDelay
|
||||
}
|
||||
var previewBox *util.EventBox
|
||||
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
|
||||
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenAddr != nil {
|
||||
// We need to start the previewer even when --preview option is not specified
|
||||
// * if HTTP server is enabled
|
||||
// * if 'preview' or 'change-preview' action is bound to a key
|
||||
// * if 'transform' action is bound to a key
|
||||
if len(opts.Preview.command) > 0 || mayTriggerPreview(opts) {
|
||||
previewBox = util.NewEventBox()
|
||||
}
|
||||
var renderer tui.Renderer
|
||||
@@ -653,6 +697,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
infoSep: opts.InfoSep,
|
||||
separator: nil,
|
||||
spinner: makeSpinner(opts.Unicode),
|
||||
promptString: opts.Prompt,
|
||||
queryLen: [2]int{0, 0},
|
||||
layout: opts.Layout,
|
||||
fullscreen: fullscreen,
|
||||
@@ -701,6 +746,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
ellipsis: opts.Ellipsis,
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
hasResultActions: false,
|
||||
hasFocusActions: false,
|
||||
hasLoadActions: false,
|
||||
triggerLoad: false,
|
||||
reading: true,
|
||||
@@ -728,10 +775,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
killChan: make(chan int),
|
||||
serverInputChan: make(chan []*action, 10),
|
||||
serverOutputChan: make(chan string),
|
||||
eventChan: make(chan tui.Event, 1),
|
||||
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
|
||||
tui: renderer,
|
||||
initFunc: func() { renderer.Init() },
|
||||
executing: util.NewAtomicBool(false)}
|
||||
executing: util.NewAtomicBool(false),
|
||||
lastAction: actStart,
|
||||
lastFocus: minItem.Index()}
|
||||
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
||||
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
||||
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
||||
@@ -750,7 +799,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
|
||||
}
|
||||
if t.unicode {
|
||||
t.borderWidth = runewidth.RuneWidth('│')
|
||||
t.borderWidth = uniseg.StringWidth("│")
|
||||
}
|
||||
if opts.Scrollbar == nil {
|
||||
if t.unicode && t.borderWidth == 1 {
|
||||
@@ -770,10 +819,17 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
var resizeActions []*action
|
||||
resizeActions, t.hasResizeActions = t.keymap[tui.Resize.AsEvent()]
|
||||
if t.tui.ShouldEmitResizeEvent() {
|
||||
t.keymap[tui.Resize.AsEvent()] = append(toActions(actClearScreen), resizeActions...)
|
||||
}
|
||||
_, t.hasResultActions = t.keymap[tui.Result.AsEvent()]
|
||||
_, t.hasFocusActions = t.keymap[tui.Focus.AsEvent()]
|
||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||
|
||||
if t.listenAddr != nil {
|
||||
err, port := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
|
||||
port, err := startHttpServer(*t.listenAddr, t.serverInputChan, t.serverOutputChan)
|
||||
if err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
@@ -788,6 +844,14 @@ func (t *Terminal) environ() []string {
|
||||
if t.listenPort != nil {
|
||||
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
|
||||
}
|
||||
env = append(env, "FZF_QUERY="+string(t.input))
|
||||
env = append(env, "FZF_ACTION="+t.lastAction.Name())
|
||||
env = append(env, "FZF_PROMPT="+string(t.promptString))
|
||||
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
|
||||
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
|
||||
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
|
||||
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
|
||||
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -817,7 +881,7 @@ func (t *Terminal) extraLines() int {
|
||||
return extra
|
||||
}
|
||||
|
||||
func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) {
|
||||
func (t *Terminal) MaxFitAndPad() (int, int) {
|
||||
_, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
||||
padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
|
||||
fit := screenHeight - padHeight - t.extraLines()
|
||||
@@ -1042,6 +1106,9 @@ func (t *Terminal) UpdateList(merger *Merger) {
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
if t.hasResultActions {
|
||||
t.eventChan <- tui.Result.AsEvent()
|
||||
}
|
||||
}
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqInfo, nil)
|
||||
@@ -1189,6 +1256,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
||||
}
|
||||
|
||||
func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
t.forcePreview = forcePreview
|
||||
screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
|
||||
width := screenWidth - marginInt[1] - marginInt[3]
|
||||
height := screenHeight - marginInt[0] - marginInt[2]
|
||||
@@ -1251,6 +1319,9 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
||||
width -= paddingInt[1] + paddingInt[3]
|
||||
height -= paddingInt[0] + paddingInt[2]
|
||||
|
||||
t.areaLines = height
|
||||
t.areaColumns = width
|
||||
|
||||
// Set up preview window
|
||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||
if forcePreview || t.needPreviewWindow() {
|
||||
@@ -2248,7 +2319,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
||||
}
|
||||
|
||||
func (t *Terminal) printAll() {
|
||||
t.resizeWindows(false)
|
||||
t.resizeWindows(t.forcePreview)
|
||||
t.printList()
|
||||
t.printPrompt()
|
||||
t.printInfo()
|
||||
@@ -2335,6 +2406,12 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
return true, match[1:], flags
|
||||
}
|
||||
|
||||
if strings.HasPrefix(match, "{fzf:") {
|
||||
// {fzf:*} are not determined by the current item
|
||||
flags.forceUpdate = true
|
||||
return false, match, flags
|
||||
}
|
||||
|
||||
skipChars := 1
|
||||
for _, char := range match[1:] {
|
||||
switch char {
|
||||
@@ -2351,7 +2428,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
flags.file = true
|
||||
skipChars++
|
||||
case 'q':
|
||||
flags.query = true
|
||||
flags.forceUpdate = true
|
||||
// query flag is not skipped
|
||||
default:
|
||||
break
|
||||
@@ -2363,14 +2440,14 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
|
||||
return false, matchWithoutFlags, flags
|
||||
}
|
||||
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) {
|
||||
func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
|
||||
for _, match := range placeholder.FindAllString(template, -1) {
|
||||
_, _, flags := parsePlaceholder(match)
|
||||
if flags.plus {
|
||||
plus = true
|
||||
}
|
||||
if flags.query {
|
||||
query = true
|
||||
if flags.forceUpdate {
|
||||
forceUpdate = true
|
||||
}
|
||||
slot = true
|
||||
}
|
||||
@@ -2397,9 +2474,30 @@ func cleanTemporaryFiles() {
|
||||
activeTempFiles = []string{}
|
||||
}
|
||||
|
||||
type replacePlaceholderParams struct {
|
||||
template string
|
||||
stripAnsi bool
|
||||
delimiter Delimiter
|
||||
printsep string
|
||||
forcePlus bool
|
||||
query string
|
||||
allItems []*Item
|
||||
lastAction actionType
|
||||
prompt string
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
|
||||
return replacePlaceholder(
|
||||
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: t.ansi,
|
||||
delimiter: t.delimiter,
|
||||
printsep: t.printsep,
|
||||
forcePlus: forcePlus,
|
||||
query: input,
|
||||
allItems: list,
|
||||
lastAction: t.lastAction,
|
||||
prompt: t.promptString,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Terminal) evaluateScrollOffset() int {
|
||||
@@ -2437,9 +2535,9 @@ func (t *Terminal) evaluateScrollOffset() int {
|
||||
return util.Max(0, base)
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
current := allItems[:1]
|
||||
selected := allItems[1:]
|
||||
func replacePlaceholder(params replacePlaceholderParams) string {
|
||||
current := params.allItems[:1]
|
||||
selected := params.allItems[1:]
|
||||
if current[0] == nil {
|
||||
current = []*Item{}
|
||||
}
|
||||
@@ -2448,7 +2546,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
// replace placeholders one by one
|
||||
return placeholder.ReplaceAllStringFunc(template, func(match string) string {
|
||||
return placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
|
||||
escaped, match, flags := parsePlaceholder(match)
|
||||
|
||||
// this function implements the effects a placeholder has on items
|
||||
@@ -2458,8 +2556,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
switch {
|
||||
case escaped:
|
||||
return match
|
||||
case match == "{q}":
|
||||
return quoteEntry(query)
|
||||
case match == "{q}" || match == "{fzf:query}":
|
||||
return quoteEntry(params.query)
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
@@ -2470,11 +2568,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
return strconv.Itoa(n)
|
||||
case flags.file:
|
||||
return item.AsString(stripAnsi)
|
||||
return item.AsString(params.stripAnsi)
|
||||
default:
|
||||
return quoteEntry(item.AsString(stripAnsi))
|
||||
return quoteEntry(item.AsString(params.stripAnsi))
|
||||
}
|
||||
}
|
||||
case match == "{fzf:action}":
|
||||
return params.lastAction.Name()
|
||||
case match == "{fzf:prompt}":
|
||||
return quoteEntry(params.prompt)
|
||||
default:
|
||||
// token type and also failover (below)
|
||||
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||
@@ -2489,15 +2591,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
replace = func(item *Item) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
|
||||
trans := Transform(tokens, ranges)
|
||||
str := joinTokens(trans)
|
||||
|
||||
// trim the last delimiter
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
delims := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if params.delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *params.delimiter.str)
|
||||
} else if params.delimiter.regex != nil {
|
||||
delims := params.delimiter.regex.FindAllStringIndex(str, -1)
|
||||
// make sure the delimiter is at the very end of the string
|
||||
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
|
||||
str = str[:delims[len(delims)-1][0]]
|
||||
@@ -2517,7 +2619,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
// apply 'replace' function over proper set of items and return result
|
||||
|
||||
items := current
|
||||
if flags.plus || forcePlus {
|
||||
if flags.plus || params.forcePlus {
|
||||
items = selected
|
||||
}
|
||||
replacements := make([]string, len(items))
|
||||
@@ -2527,7 +2629,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
|
||||
}
|
||||
|
||||
if flags.file {
|
||||
return writeTemporaryFile(replacements, printsep)
|
||||
return writeTemporaryFile(replacements, params.printsep)
|
||||
}
|
||||
return strings.Join(replacements, " ")
|
||||
})
|
||||
@@ -2609,8 +2711,8 @@ func (t *Terminal) currentItem() *Item {
|
||||
|
||||
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||
current := t.currentItem()
|
||||
slot, plus, query := hasPreviewFlags(template)
|
||||
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
slot, plus, forceUpdate := hasPreviewFlags(template)
|
||||
if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) {
|
||||
return current != nil, []*Item{current, current}
|
||||
}
|
||||
|
||||
@@ -2704,6 +2806,13 @@ func (t *Terminal) pwindowSize() tui.TermSize {
|
||||
return size
|
||||
}
|
||||
|
||||
func (t *Terminal) currentIndex() int32 {
|
||||
if currentItem := t.currentItem(); currentItem != nil {
|
||||
return currentItem.Index()
|
||||
}
|
||||
return minItem.Index()
|
||||
}
|
||||
|
||||
// Loop is called to start Terminal I/O
|
||||
func (t *Terminal) Loop() {
|
||||
// prof := profile.Start(profile.ProfilePath("/tmp/"))
|
||||
@@ -2750,14 +2859,16 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
}()
|
||||
|
||||
resizeChan := make(chan os.Signal, 1)
|
||||
notifyOnResize(resizeChan) // Non-portable
|
||||
go func() {
|
||||
for {
|
||||
<-resizeChan
|
||||
t.reqBox.Set(reqResize, nil)
|
||||
}
|
||||
}()
|
||||
if !t.tui.ShouldEmitResizeEvent() {
|
||||
resizeChan := make(chan os.Signal, 1)
|
||||
notifyOnResize(resizeChan) // Non-portable
|
||||
go func() {
|
||||
for {
|
||||
<-resizeChan
|
||||
t.reqBox.Set(reqResize, nil)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
t.mutex.Lock()
|
||||
t.initFunc()
|
||||
@@ -2794,6 +2905,7 @@ func (t *Terminal) Loop() {
|
||||
for {
|
||||
var items []*Item
|
||||
var commandTemplate string
|
||||
var pwindow tui.Window
|
||||
var pwindowSize tui.TermSize
|
||||
initialOffset := 0
|
||||
t.previewBox.Wait(func(events *util.Events) {
|
||||
@@ -2804,6 +2916,7 @@ func (t *Terminal) Loop() {
|
||||
commandTemplate = request.template
|
||||
initialOffset = request.scrollOffset
|
||||
items = request.list
|
||||
pwindow = request.pwindow
|
||||
pwindowSize = request.pwindowSize
|
||||
}
|
||||
}
|
||||
@@ -2823,8 +2936,8 @@ func (t *Terminal) Loop() {
|
||||
env = append(env, "FZF_PREVIEW_"+lines)
|
||||
env = append(env, columns)
|
||||
env = append(env, "FZF_PREVIEW_"+columns)
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_TOP=%d", t.tui.Top()+t.pwindow.Top()))
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_LEFT=%d", t.pwindow.Left()))
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_TOP=%d", t.tui.Top()+pwindow.Top()))
|
||||
env = append(env, fmt.Sprintf("FZF_PREVIEW_LEFT=%d", pwindow.Left()))
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
@@ -2952,7 +3065,7 @@ func (t *Terminal) Loop() {
|
||||
if len(command) > 0 && t.canPreview() {
|
||||
_, list := t.buildPlusList(command, false)
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2986,18 +3099,15 @@ func (t *Terminal) Loop() {
|
||||
t.printInfo()
|
||||
case reqList:
|
||||
t.printList()
|
||||
var currentIndex int32 = minItem.Index()
|
||||
currentItem := t.currentItem()
|
||||
if currentItem != nil {
|
||||
currentIndex = currentItem.Index()
|
||||
}
|
||||
currentIndex := t.currentIndex()
|
||||
focusChanged := focusedIndex != currentIndex
|
||||
if focusChanged && t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
t.printInfo()
|
||||
}
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
|
||||
t.serverInputChan <- onFocus
|
||||
if t.hasFocusActions && focusChanged && currentIndex != t.lastFocus {
|
||||
t.lastFocus = currentIndex
|
||||
t.eventChan <- tui.Focus.AsEvent()
|
||||
}
|
||||
if focusChanged || version != t.version {
|
||||
version = t.version
|
||||
@@ -3029,6 +3139,9 @@ func (t *Terminal) Loop() {
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
refreshPreview(t.previewOpts.command)
|
||||
}
|
||||
if req == reqResize && t.hasResizeActions {
|
||||
t.eventChan <- tui.Resize.AsEvent()
|
||||
}
|
||||
case reqClose:
|
||||
exit(func() int {
|
||||
if t.output() {
|
||||
@@ -3048,7 +3161,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
t.previewer.lines = result.lines
|
||||
t.previewer.spinner = result.spinner
|
||||
if t.previewer.following.Enabled() {
|
||||
if t.hasPreviewWindow() && t.previewer.following.Enabled() {
|
||||
t.previewer.offset = util.Max(t.previewer.offset, len(t.previewer.lines)-(t.pwindow.Height()-t.previewOpts.headerLines))
|
||||
} else if result.offset >= 0 {
|
||||
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
|
||||
@@ -3111,7 +3224,11 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
select {
|
||||
case event = <-t.eventChan:
|
||||
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||
if t.tui.ShouldEmitResizeEvent() {
|
||||
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero)
|
||||
} else {
|
||||
needBarrier = !event.Is(tui.Load, tui.Result, tui.Focus, tui.One, tui.Zero, tui.Resize)
|
||||
}
|
||||
case serverActions := <-t.serverInputChan:
|
||||
event = tui.Invalid.AsEvent()
|
||||
if t.listenAddr == nil || t.listenAddr.IsLocal() || t.listenUnsafe {
|
||||
@@ -3186,17 +3303,26 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
|
||||
var doAction func(*action) bool
|
||||
doActions := func(actions []*action) bool {
|
||||
var doActions func(actions []*action) bool
|
||||
doActions = func(actions []*action) bool {
|
||||
currentIndex := t.currentIndex()
|
||||
for _, action := range actions {
|
||||
if !doAction(action) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs {
|
||||
if newIndex := t.currentIndex(); newIndex != currentIndex {
|
||||
t.lastFocus = newIndex
|
||||
return doActions(onFocus)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
doAction = func(a *action) bool {
|
||||
switch a.t {
|
||||
case actIgnore:
|
||||
case actIgnore, actStart, actClick:
|
||||
case actResponse:
|
||||
t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a))
|
||||
case actBecome:
|
||||
@@ -3250,12 +3376,15 @@ func (t *Terminal) Loop() {
|
||||
if valid {
|
||||
t.cancelPreview()
|
||||
t.previewBox.Set(reqPreviewEnqueue,
|
||||
previewRequest{t.previewOpts.command, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
previewRequest{t.previewOpts.command, t.pwindow, t.pwindowSize(), t.evaluateScrollOffset(), list})
|
||||
}
|
||||
} else {
|
||||
// Discard the preview content so that it won't accidentally appear
|
||||
// when preview window is re-enabled and previewDelay is triggered
|
||||
t.previewer.lines = nil
|
||||
|
||||
// Also kill the preview process if it's still running
|
||||
t.cancelPreview()
|
||||
}
|
||||
}
|
||||
case actTogglePreviewWrap:
|
||||
@@ -3267,6 +3396,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actTransformPrompt:
|
||||
prompt := t.executeCommand(a.a, false, true, true, true)
|
||||
t.promptString = prompt
|
||||
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
||||
req(reqPrompt)
|
||||
case actTransformQuery:
|
||||
@@ -3343,6 +3473,10 @@ func (t *Terminal) Loop() {
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actTransform:
|
||||
body := t.executeCommand(a.a, false, true, true, false)
|
||||
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
|
||||
return doActions(actions)
|
||||
case actTransformBorderLabel:
|
||||
if t.border != nil {
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
@@ -3356,10 +3490,13 @@ func (t *Terminal) Loop() {
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
case actChangePrompt:
|
||||
t.promptString = a.a
|
||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||
req(reqPrompt)
|
||||
case actPreview:
|
||||
updatePreviewWindow(true)
|
||||
if !t.hasPreviewWindow() {
|
||||
updatePreviewWindow(true)
|
||||
}
|
||||
refreshPreview(a.a)
|
||||
case actRefreshPreview:
|
||||
refreshPreview(t.previewOpts.command)
|
||||
@@ -3373,7 +3510,7 @@ func (t *Terminal) Loop() {
|
||||
req(reqQuit)
|
||||
case actDeleteChar:
|
||||
t.delChar()
|
||||
case actDeleteCharEOF:
|
||||
case actDeleteCharEof:
|
||||
if !t.delChar() && t.cx == 0 {
|
||||
req(reqQuit)
|
||||
}
|
||||
@@ -3387,7 +3524,7 @@ func (t *Terminal) Loop() {
|
||||
t.input = []rune{}
|
||||
t.cx = 0
|
||||
}
|
||||
case actBackwardDeleteCharEOF:
|
||||
case actBackwardDeleteCharEof:
|
||||
if len(t.input) == 0 {
|
||||
req(reqQuit)
|
||||
} else if t.cx > 0 {
|
||||
@@ -3494,6 +3631,12 @@ func (t *Terminal) Loop() {
|
||||
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
|
||||
req(reqClose)
|
||||
}
|
||||
case actAcceptOrPrintQuery:
|
||||
if len(t.selected) > 0 || t.merger.Length() > 0 {
|
||||
req(reqClose)
|
||||
} else {
|
||||
req(reqPrintQuery)
|
||||
}
|
||||
case actClearScreen:
|
||||
req(reqFullRedraw)
|
||||
case actClearQuery:
|
||||
@@ -3600,7 +3743,7 @@ func (t *Terminal) Loop() {
|
||||
t.yanked = copySlice(t.input[t.cx:])
|
||||
t.input = t.input[:t.cx]
|
||||
}
|
||||
case actRune:
|
||||
case actChar:
|
||||
prefix := copySlice(t.input[:t.cx])
|
||||
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
|
||||
t.cx++
|
||||
@@ -3628,6 +3771,12 @@ func (t *Terminal) Loop() {
|
||||
t.track = trackEnabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actShowHeader:
|
||||
t.headerVisible = true
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actHideHeader:
|
||||
t.headerVisible = false
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actToggleHeader:
|
||||
t.headerVisible = !t.headerVisible
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
@@ -3797,8 +3946,8 @@ func (t *Terminal) Loop() {
|
||||
// We run the command even when there's no match
|
||||
// 1. If the template doesn't have any slots
|
||||
// 2. If the template has {q}
|
||||
slot, _, query := hasPreviewFlags(a.a)
|
||||
valid = !slot || query
|
||||
slot, _, forceUpdate := hasPreviewFlags(a.a)
|
||||
valid = !slot || forceUpdate
|
||||
}
|
||||
if valid {
|
||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||
@@ -3842,11 +3991,18 @@ func (t *Terminal) Loop() {
|
||||
|
||||
// Full redraw
|
||||
if !currentPreviewOpts.sameLayout(t.previewOpts) {
|
||||
wasHidden := t.pwindow == nil
|
||||
// Preview command can be running in the background if the size of
|
||||
// the preview window is 0 but not 'hidden'
|
||||
wasHidden := currentPreviewOpts.hidden
|
||||
updatePreviewWindow(false)
|
||||
if wasHidden && t.hasPreviewWindow() {
|
||||
// Restart
|
||||
refreshPreview(t.previewOpts.command)
|
||||
} else if t.previewOpts.hidden {
|
||||
// Cancel
|
||||
t.cancelPreview()
|
||||
} else {
|
||||
// Refresh
|
||||
req(reqPreviewRefresh)
|
||||
}
|
||||
} else if !currentPreviewOpts.sameContentLayout(t.previewOpts) {
|
||||
@@ -3878,6 +4034,10 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !processExecution(a.t) {
|
||||
t.lastAction = a.t
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -3891,7 +4051,7 @@ func (t *Terminal) Loop() {
|
||||
actions = t.keymap[event.Comparable()]
|
||||
}
|
||||
if len(actions) == 0 && event.Type == tui.Rune {
|
||||
doAction(&action{t: actRune})
|
||||
doAction(&action{t: actChar})
|
||||
} else if !doActions(actions) {
|
||||
continue
|
||||
}
|
||||
@@ -3922,8 +4082,8 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
|
||||
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
|
||||
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
||||
if q {
|
||||
_, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
|
||||
if forceUpdate {
|
||||
t.version++
|
||||
}
|
||||
}
|
||||
@@ -3932,10 +4092,15 @@ func (t *Terminal) Loop() {
|
||||
req(reqPrompt)
|
||||
}
|
||||
|
||||
reload := changed || newCommand != nil
|
||||
var reloadRequest *searchRequest
|
||||
if reload {
|
||||
reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, environ: t.environ(), changed: changed}
|
||||
}
|
||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||
|
||||
if changed || newCommand != nil {
|
||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
|
||||
if reload {
|
||||
t.eventBox.Set(EvtSearchNew, *reloadRequest)
|
||||
}
|
||||
for _, event := range events {
|
||||
t.reqBox.Set(event, nil)
|
||||
|
@@ -12,6 +12,20 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: stripAnsi,
|
||||
delimiter: delimiter,
|
||||
printsep: printsep,
|
||||
forcePlus: forcePlus,
|
||||
query: query,
|
||||
allItems: allItems,
|
||||
lastAction: actBackwardDeleteCharEof,
|
||||
prompt: "prompt",
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
@@ -52,90 +66,90 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
*/
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with "'" delimiter
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with regex delimiter
|
||||
regex = regexp.MustCompile(`\w+`)
|
||||
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||
|
||||
// Regex delimiter
|
||||
regex = regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
|
||||
|
||||
/*
|
||||
@@ -155,7 +169,6 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
}
|
||||
stripAnsi := false
|
||||
printsep = "\n"
|
||||
forcePlus := false
|
||||
query := "sample query"
|
||||
|
||||
@@ -198,18 +211,23 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
|
||||
|
||||
// IV. escaping placeholder
|
||||
templateToOutput[`\{}`] = `{}`
|
||||
templateToOutput[`\{q}`] = `{q}`
|
||||
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
|
||||
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
|
||||
templateToOutput[`\{++}`] = `{++}`
|
||||
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||
|
||||
for giveTemplate, wantOutput := range templateToOutput {
|
||||
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
checkFormat(wantOutput)
|
||||
}
|
||||
for giveTemplate, wantOutput := range templateToFile {
|
||||
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
|
||||
data, err := readFile(path)
|
||||
if err != nil {
|
||||
@@ -563,7 +581,7 @@ func testCommands(t *testing.T, tests []testCase) {
|
||||
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholder(
|
||||
gotOutput := replacePlaceholderTest(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
@@ -605,7 +623,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
|
||||
if flags.file {
|
||||
encoded += "f"
|
||||
}
|
||||
if flags.query {
|
||||
if flags.forceUpdate { // FIXME
|
||||
encoded += "q"
|
||||
}
|
||||
return encoded
|
||||
|
@@ -11,6 +11,20 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var escaper *strings.Replacer
|
||||
|
||||
func init() {
|
||||
tokens := strings.Split(os.Getenv("SHELL"), "/")
|
||||
if tokens[len(tokens)-1] == "fish" {
|
||||
// https://fishshell.com/docs/current/language.html#quotes
|
||||
// > The only meaningful escape sequences in single quotes are \', which
|
||||
// > escapes a single quote and \\, which escapes the backslash symbol.
|
||||
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
|
||||
} else {
|
||||
escaper = strings.NewReplacer("'", "'\\''")
|
||||
}
|
||||
}
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGWINCH)
|
||||
}
|
||||
@@ -29,5 +43,5 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
return "'" + escaper.Replace(entry) + "'"
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
|
||||
"golang.org/x/term"
|
||||
@@ -695,6 +694,10 @@ func (r *LightRenderer) NeedScrollbarRedraw() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) ShouldEmitResizeEvent() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) RefreshWindows(windows []Window) {
|
||||
r.flush()
|
||||
}
|
||||
@@ -804,14 +807,26 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
hw := runeWidth(w.border.top)
|
||||
pad := repeat(' ', w.width/hw)
|
||||
|
||||
w.Move(0, 0)
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(color, repeat(w.border.top, w.width/hw))
|
||||
} else {
|
||||
w.CPrint(color, pad)
|
||||
}
|
||||
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, pad)
|
||||
}
|
||||
|
||||
w.Move(w.height-1, 0)
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
|
||||
} else {
|
||||
w.CPrint(color, pad)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,13 +857,13 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
hw := runewidth.RuneWidth(w.border.top)
|
||||
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight)
|
||||
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight)
|
||||
hw := runeWidth(w.border.top)
|
||||
tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
|
||||
bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
|
||||
rem := (w.width - tcw) % hw
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
|
||||
if !onlyHorizontal {
|
||||
vw := runewidth.RuneWidth(w.border.left)
|
||||
vw := runeWidth(w.border.left)
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.left))
|
||||
@@ -1020,7 +1035,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
} else if rs[0] == '\r' {
|
||||
w++
|
||||
} else {
|
||||
w = runewidth.StringWidth(str)
|
||||
w = uniseg.StringWidth(str)
|
||||
}
|
||||
width += w
|
||||
|
||||
|
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
@@ -144,6 +143,7 @@ func (a Attr) Merge(b Attr) Attr {
|
||||
var (
|
||||
_screen tcell.Screen
|
||||
_prevMouseButton tcell.ButtonMask
|
||||
_initialResize bool = true
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) initScreen() {
|
||||
@@ -203,6 +203,10 @@ func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Refresh() {
|
||||
// noop
|
||||
}
|
||||
@@ -217,6 +221,12 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
// Ignore the first resize event
|
||||
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
||||
if _initialResize {
|
||||
_initialResize = false
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Resize, 0, nil}
|
||||
|
||||
// process mouse events:
|
||||
@@ -534,7 +544,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
w.drawBorder(false)
|
||||
w.Erase()
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -551,8 +561,8 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Erase() {
|
||||
w.drawBorder(false)
|
||||
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
|
||||
w.drawBorder(false)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EraseMaybe() bool {
|
||||
@@ -738,7 +748,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
style = w.normal.style()
|
||||
}
|
||||
|
||||
hw := runewidth.RuneWidth(w.borderStyle.top)
|
||||
hw := runeWidth(w.borderStyle.top)
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||
max := right - 2*hw
|
||||
@@ -773,7 +783,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
|
||||
vw := runewidth.RuneWidth(w.borderStyle.right)
|
||||
vw := runeWidth(w.borderStyle.right)
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||
}
|
||||
@@ -782,8 +792,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
_screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
}
|
||||
}
|
||||
|
@@ -182,6 +182,7 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
r.Init()
|
||||
|
||||
// run and evaluate the tests
|
||||
initialResizeAsInvalid := true
|
||||
for _, test := range tests {
|
||||
// generate key event
|
||||
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
|
||||
@@ -191,8 +192,9 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// process the event in fzf and evaluate the test
|
||||
gotEvent := r.GetChar()
|
||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||
for gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||
t.Logf("Resize as Invalid swallowed")
|
||||
initialResizeAsInvalid = false
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||
|
@@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// Types of user action
|
||||
@@ -105,6 +107,7 @@ const (
|
||||
Focus
|
||||
One
|
||||
Zero
|
||||
Result
|
||||
|
||||
AltBS
|
||||
|
||||
@@ -491,6 +494,7 @@ type Renderer interface {
|
||||
Close()
|
||||
PassThrough(string)
|
||||
NeedScrollbarRedraw() bool
|
||||
ShouldEmitResizeEvent() bool
|
||||
|
||||
GetChar() Event
|
||||
|
||||
@@ -811,3 +815,7 @@ func initPalette(theme *ColorTheme) {
|
||||
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
||||
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
|
||||
}
|
||||
|
||||
func runeWidth(r rune) int {
|
||||
return uniseg.StringWidth(string(r))
|
||||
}
|
||||
|
@@ -7,13 +7,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// StringWidth returns string width where each CR/LF character takes 1 column
|
||||
func StringWidth(s string) int {
|
||||
return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
|
||||
return uniseg.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
|
||||
}
|
||||
|
||||
// RunesWidth returns runes width
|
||||
@@ -165,7 +164,7 @@ func RepeatToFill(str string, length int, limit int) string {
|
||||
output := strings.Repeat(str, times)
|
||||
if rest > 0 {
|
||||
for _, r := range str {
|
||||
rest -= runewidth.RuneWidth(r)
|
||||
rest -= uniseg.StringWidth(string(r))
|
||||
if rest < 0 {
|
||||
break
|
||||
}
|
||||
|
@@ -164,6 +164,18 @@ func TestRunesWidth(t *testing.T) {
|
||||
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
|
||||
}
|
||||
}
|
||||
for _, input := range []struct {
|
||||
s string
|
||||
w int
|
||||
}{
|
||||
{"▶", 1},
|
||||
{"▶️", 2},
|
||||
} {
|
||||
width, _ := RunesWidth([]rune(input.s), 0, 0, 100)
|
||||
if width != input.w {
|
||||
t.Errorf("Expected width of %s: %d, actual: %d", input.s, input.w, width)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
|
141
test/test_go.rb
141
test/test_go.rb
@@ -741,6 +741,12 @@ class TestGoFZF < TestBase
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
writelines(tempname, ['/bar/baz', '/foo/bar/baz'])
|
||||
assert_equal [
|
||||
'/foo/bar/baz',
|
||||
'/bar/baz'
|
||||
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_length_with_nth
|
||||
@@ -958,26 +964,40 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_execute
|
||||
output = '/tmp/fzf-test-execute'
|
||||
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}"]
|
||||
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
|
||||
writelines(tempname, %w[foo'bar foo"bar foo$bar])
|
||||
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
|
||||
tmux.until { |lines| assert_equal ' 3/3', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :C
|
||||
ready.call('C')
|
||||
|
||||
tmux.send_keys 'barfoo'
|
||||
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
|
||||
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal %w[
|
||||
/foo'bar/ /foo'bar/
|
||||
/foo"barfoo"bar/ /foo"barfoo"bar/
|
||||
/foo'bar/ /foo'barfoo'bar/
|
||||
/foo"bar/ /foo"barfoo"bar/
|
||||
/foo$barfoo$barfoo$bar/
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
@@ -987,17 +1007,28 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})"]
|
||||
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
|
||||
writelines(tempname, %w[foo'bar foo"bar foo$bar foobar])
|
||||
tmux.send_keys "cat #{tempname} | #{fzf(opts)}", :Enter
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
|
||||
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal [
|
||||
@@ -1215,7 +1246,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_toggle_header
|
||||
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border", :Enter
|
||||
tmux.send_keys "seq 4 | #{FZF} --header-lines 2 --header foo --bind space:toggle-header --header-first --height 10 --border rounded", :Enter
|
||||
before = <<~OUTPUT
|
||||
╭───────
|
||||
│
|
||||
@@ -1776,6 +1807,35 @@ class TestGoFZF < TestBase
|
||||
assert_equal %w[foo], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_without_match
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys 99_999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[99999], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_with_match
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys '^99$'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[99], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_accept_or_print_query_with_multi_selection
|
||||
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query --multi')}), :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||
tmux.send_keys 99_999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal %w[1 2 3], readonce.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_preview_update_on_select
|
||||
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
|
||||
:Enter
|
||||
@@ -1858,7 +1918,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_reload
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
|
||||
tmux.until { |lines| assert_equal 998, lines.match_count }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until do |lines|
|
||||
@@ -1987,6 +2047,13 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal '> RAB', lines[-1] }
|
||||
end
|
||||
|
||||
def test_transform
|
||||
tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter
|
||||
tmux.until { |lines| assert_equal 'start', lines[-1] }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 'up', lines[-1] }
|
||||
end
|
||||
|
||||
def test_clear_selection
|
||||
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
@@ -2118,14 +2185,15 @@ class TestGoFZF < TestBase
|
||||
file = Tempfile.new('fzf-follow')
|
||||
file.sync = true
|
||||
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~3'), :Enter
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
|
||||
tmux.until { |lines| lines.item_count == 100 }
|
||||
|
||||
# Write to the temporary file, and check if the preview window is showing
|
||||
# the last line of the file
|
||||
tmux.until { |lines| assert_includes lines[1], 'start' }
|
||||
3.times { file.puts _1 } # header lines
|
||||
1000.times { file.puts _1 }
|
||||
tmux.until { |lines| assert_includes lines[1], '/1003' }
|
||||
tmux.until { |lines| assert_includes lines[1], '/1004' }
|
||||
tmux.until { |lines| assert_includes lines[-2], '999' }
|
||||
|
||||
# Scroll the preview window and fzf should stop following the file content
|
||||
@@ -2133,7 +2201,7 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines[-2], '998' }
|
||||
file.puts 'foo', 'bar'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1005'
|
||||
assert_includes lines[1], '/1006'
|
||||
assert_includes lines[-2], '998'
|
||||
end
|
||||
|
||||
@@ -2146,7 +2214,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
file.puts 'baz'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1006'
|
||||
assert_includes lines[1], '/1007'
|
||||
assert_includes lines[-2], 'baz'
|
||||
end
|
||||
|
||||
@@ -2155,7 +2223,7 @@ class TestGoFZF < TestBase
|
||||
wait { assert_includes lines[-2], 'bar' }
|
||||
file.puts 'aaa'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1007'
|
||||
assert_includes lines[1], '/1008'
|
||||
assert_includes lines[-2], 'bar'
|
||||
end
|
||||
|
||||
@@ -2164,7 +2232,7 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
|
||||
file.puts 'bbb'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1008'
|
||||
assert_includes lines[1], '/1009'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
|
||||
@@ -2172,7 +2240,7 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Space
|
||||
file.puts 'ccc', 'ddd'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1010'
|
||||
assert_includes lines[1], '/1011'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
rescue StandardError
|
||||
@@ -2514,6 +2582,7 @@ class TestGoFZF < TestBase
|
||||
def test_change_preview_window_rotate
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
|
||||
"a:change-preview-window(right|down|up|hidden|)'", :Enter
|
||||
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
|
||||
3.times do
|
||||
tmux.until { |lines| lines[0].start_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
@@ -2574,7 +2643,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_height_range_fit
|
||||
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border', :Enter
|
||||
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
|
||||
expected = <<~OUTPUT
|
||||
╭──────────
|
||||
│ 3
|
||||
@@ -2587,7 +2656,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_height_range_fit_preview_above
|
||||
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border --preview "seq {}" --preview-window up,60%', :Enter
|
||||
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded --preview-window border-rounded --preview "seq {}" --preview-window up,60%', :Enter
|
||||
expected = <<~OUTPUT
|
||||
╭──────────
|
||||
│ ╭────────
|
||||
@@ -2643,7 +2712,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_height_range_overflow
|
||||
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border', :Enter
|
||||
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
|
||||
expected = <<~OUTPUT
|
||||
╭──────────────
|
||||
│ 2
|
||||
@@ -2669,12 +2738,26 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[2]]') }
|
||||
tmux.send_keys :X
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[1]]') }
|
||||
tmux.send_keys :X
|
||||
tmux.until { |lines| assert_includes(lines[-1], '[[]]') }
|
||||
tmux.send_keys '?'
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes(lines[-1], '[[1]]') }
|
||||
end
|
||||
|
||||
def test_result_event
|
||||
tmux.send_keys '(echo 0; seq 10) | fzf --bind "result:pos(2)"', :Enter
|
||||
tmux.until { |lines| assert_equal 11, lines.item_count }
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_includes lines, '> 9' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
end
|
||||
|
||||
def test_labels_center
|
||||
tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter
|
||||
tmux.until do
|
||||
@@ -2694,7 +2777,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_labels_left
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos 2 --preview : --preview-label barfoo --preview-label-pos 2', :Enter
|
||||
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos 2 --preview : --preview-label barfoo --preview-label-pos 2', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '╭foobar─')
|
||||
assert_includes(_1[1], '╭barfoo─')
|
||||
@@ -2702,7 +2785,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_labels_right
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos -2 --preview : --preview-label barfoo --preview-label-pos -2', :Enter
|
||||
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos -2 --preview : --preview-label barfoo --preview-label-pos -2', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobar╮')
|
||||
assert_includes(_1[1], '─barfoo╮')
|
||||
@@ -2710,7 +2793,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_labels_bottom
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --border-label-pos 2:bottom --preview : --preview-label barfoo --preview-label-pos -2:bottom', :Enter
|
||||
tmux.send_keys ': | fzf --border rounded --preview-window border-rounded --border-label foobar --border-label-pos 2:bottom --preview : --preview-label barfoo --preview-label-pos -2:bottom', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[-1], '╰foobar─')
|
||||
assert_includes(_1[-2], '─barfoo╯')
|
||||
@@ -2839,7 +2922,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_no_extra_newline_issue_3209
|
||||
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
|
||||
tmux.send_keys(%(seq 100 | #{FZF} --height 10 --preview-window up,wrap,border-rounded --preview 'printf "─%.0s" $(seq 1 "$((FZF_PREVIEW_COLUMNS - 5))"); printf $"\\e[7m%s\\e[0m" title; echo; echo something'), :Enter)
|
||||
expected = <<~OUTPUT
|
||||
╭──────────
|
||||
│ ─────────
|
||||
@@ -3000,6 +3083,11 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_delete_with_modifiers
|
||||
if ENV['GITHUB_ACTION']
|
||||
# Expected: "[3]"
|
||||
# Actual: "[]3;5~"
|
||||
skip('CTRL-DELETE is not properly handled in GitHub Actions environment')
|
||||
end
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-delete:up+up,shift-delete:down,focus:transform-prompt:echo [{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'C-Delete'
|
||||
@@ -3020,6 +3108,13 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :x
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||
end
|
||||
|
||||
def test_preview_window_hidden_on_focus
|
||||
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
|
Reference in New Issue
Block a user