mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 23:03:47 -05:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb76893e18 | ||
|
|
88d812fe82 | ||
|
|
77f9f4664a | ||
|
|
5c2f85c39e | ||
|
|
ac4d22cd12 | ||
|
|
cf95e44cb4 | ||
|
|
65dd2bb429 | ||
|
|
6be855be6a | ||
|
|
b6e3f4423b | ||
|
|
0c61d81713 | ||
|
|
7c6f5dba63 | ||
|
|
44cfc7e62a | ||
|
|
96670d5f16 | ||
|
|
36b971ee4e | ||
|
|
f1a9629652 | ||
|
|
20230402d0 | ||
|
|
5c2c3a6c88 | ||
|
|
fb019d43bf | ||
|
|
025aa33773 | ||
|
|
302e21fd58 | ||
|
|
211512ae64 | ||
|
|
8ec917b1c3 | ||
|
|
1c7534f009 | ||
|
|
ae745d9397 | ||
|
|
60f37aae2f | ||
|
|
d7daf5f724 | ||
|
|
e5103d9429 | ||
|
|
8fecb29848 | ||
|
|
290ea6179d | ||
|
|
9695a40fc9 | ||
|
|
1913b95227 | ||
|
|
a874aea692 | ||
|
|
69c52099e7 | ||
|
|
cfc0747d5d | ||
|
|
fcd7e8768d | ||
|
|
3c34dd8275 | ||
|
|
1116e481be | ||
|
|
63cf9d04de | ||
|
|
3364d4d147 | ||
|
|
57ad21e4bd | ||
|
|
414f87981f | ||
|
|
b1459c79cf | ||
|
|
352ea07226 | ||
|
|
27018787af | ||
|
|
4e305eca26 | ||
|
|
9e9c0ceaf4 | ||
|
|
b3bf18b1c0 | ||
|
|
b1619f675f | ||
|
|
96c3de12eb | ||
|
|
719dbb8bae | ||
|
|
f38a7f7f8f | ||
|
|
6ea38b4438 | ||
|
|
f7447aece1 | ||
|
|
aa2b9ec476 | ||
|
|
3ee00f8bc2 | ||
|
|
fccab60a5c | ||
|
|
0f4af38457 | ||
|
|
aef39f1160 |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.19
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18
|
go-version: 1.18
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/typos.yml
vendored
Normal file
10
.github/workflows/typos.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: "Spell Check"
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typos:
|
||||||
|
name: Spell Check with Typos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: crate-ci/typos@v1.13.16
|
||||||
15
.github/workflows/winget.yml
vendored
Normal file
15
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: Publish to Winget
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest # Action can only run on Windows
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
|
with:
|
||||||
|
identifier: junegunn.fzf
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
@@ -74,6 +74,7 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
- loong64
|
- loong64
|
||||||
- ppc64le
|
- ppc64le
|
||||||
|
- s390x
|
||||||
goarm:
|
goarm:
|
||||||
- 5
|
- 5
|
||||||
- 6
|
- 6
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
golang 1.19
|
golang 1.20.2
|
||||||
|
|||||||
144
ADVANCED.md
144
ADVANCED.md
@@ -1,30 +1,33 @@
|
|||||||
Advanced fzf examples
|
Advanced fzf examples
|
||||||
======================
|
======================
|
||||||
|
|
||||||
*(Last update: 2022/08/25)*
|
* *Last update: 2023/02/15*
|
||||||
|
* *Requires fzf 0.38.0 or above*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Introduction](#introduction)
|
* [Introduction](#introduction)
|
||||||
* [Screen Layout](#screen-layout)
|
* [Screen Layout](#screen-layout)
|
||||||
* [`--height`](#--height)
|
* [`--height`](#--height)
|
||||||
* [`fzf-tmux`](#fzf-tmux)
|
* [`fzf-tmux`](#fzf-tmux)
|
||||||
* [Popup window support](#popup-window-support)
|
* [Popup window support](#popup-window-support)
|
||||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
* [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)
|
* [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 between data sources](#toggling-between-data-sources)
|
||||||
* [Ripgrep integration](#ripgrep-integration)
|
* [Ripgrep integration](#ripgrep-integration)
|
||||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
* [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 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](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||||
* [Log tailing](#log-tailing)
|
* [Log tailing](#log-tailing)
|
||||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||||
* [Branches](#branches)
|
* [Branches](#branches)
|
||||||
* [Commit hashes](#commit-hashes)
|
* [Commit hashes](#commit-hashes)
|
||||||
* [Color themes](#color-themes)
|
* [Color themes](#color-themes)
|
||||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||||
|
|
||||||
<!-- vim-markdown-toc -->
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
@@ -236,15 +239,13 @@ file called `rfv`.
|
|||||||
# 1. Search for text in files using Ripgrep
|
# 1. Search for text in files using Ripgrep
|
||||||
# 2. Interactively narrow down the list using fzf
|
# 2. Interactively narrow down the list using fzf
|
||||||
# 3. Open the file in Vim
|
# 3. Open the file in Vim
|
||||||
IFS=: read -ra selected < <(
|
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
fzf --ansi \
|
||||||
fzf --ansi \
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
--delimiter : \
|
||||||
--delimiter : \
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
)
|
|
||||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And run it with an initial query string.
|
And run it with an initial query string.
|
||||||
@@ -307,8 +308,12 @@ I know it's a lot to digest, let's try to break down the code.
|
|||||||
position in the window
|
position in the window
|
||||||
- `~3` makes the top three lines fixed header so that they are always
|
- `~3` makes the top three lines fixed header so that they are always
|
||||||
visible regardless of the scroll offset
|
visible regardless of the scroll offset
|
||||||
- Once we selected a line, we open the file with `vim` (`vim
|
- Instead of using shell script to process the final output of fzf, we use
|
||||||
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
|
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
|
||||||
|
into a new process that opens the file with `vim` (`vim {1}`) and move the
|
||||||
|
cursor to the line (`+{2}`).
|
||||||
|
|
||||||
|
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
|
||||||
|
|
||||||
### Using fzf as interactive Ripgrep launcher
|
### Using fzf as interactive Ripgrep launcher
|
||||||
|
|
||||||
@@ -331,16 +336,14 @@ projects, and it will free up memory as you narrow down the results.
|
|||||||
# 3. Open the file in Vim
|
# 3. Open the file in Vim
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
IFS=: read -ra selected < <(
|
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
fzf --ansi \
|
||||||
fzf --ansi \
|
--disabled --query "$INITIAL_QUERY" \
|
||||||
--disabled --query "$INITIAL_QUERY" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--delimiter : \
|
||||||
--delimiter : \
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
)
|
|
||||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
@@ -358,8 +361,6 @@ IFS=: read -ra selected < <(
|
|||||||
|
|
||||||
### Switching to fzf-only search mode
|
### Switching to fzf-only search mode
|
||||||
|
|
||||||
*(Requires fzf 0.27.1 or above)*
|
|
||||||
|
|
||||||
In the previous example, we lost fuzzy matching capability as we completely
|
In the previous example, we lost fuzzy matching capability as we completely
|
||||||
delegated search functionality to Ripgrep. But we can dynamically switch to
|
delegated search functionality to Ripgrep. But we can dynamically switch to
|
||||||
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||||
@@ -375,19 +376,17 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
|||||||
# 3. Open the file in Vim
|
# 3. Open the file in Vim
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
IFS=: read -ra selected < <(
|
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
fzf --ansi \
|
||||||
fzf --ansi \
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
--disabled --query "$INITIAL_QUERY" \
|
||||||
--disabled --query "$INITIAL_QUERY" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
--prompt '1. ripgrep> ' \
|
||||||
--prompt '1. ripgrep> ' \
|
--delimiter : \
|
||||||
--delimiter : \
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
)
|
|
||||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Phase 1. Filtering with Ripgrep
|
* Phase 1. Filtering with Ripgrep
|
||||||
@@ -408,10 +407,8 @@ IFS=: read -ra selected < <(
|
|||||||
|
|
||||||
### Switching between Ripgrep mode and fzf mode
|
### Switching between Ripgrep mode and fzf mode
|
||||||
|
|
||||||
*(Requires fzf 0.30.0 or above)*
|
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
|
||||||
|
that were previously "unbound" via `unbind`.
|
||||||
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
|
|
||||||
previously "unbound" via `unbind`.
|
|
||||||
|
|
||||||
This is an improved version of the previous example that allows us to switch
|
This is an improved version of the previous example that allows us to switch
|
||||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||||
@@ -421,25 +418,34 @@ CTRL-F.
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
||||||
|
rm -f /tmp/rg-fzf-{r,f}
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
IFS=: read -ra selected < <(
|
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
fzf --ansi \
|
||||||
fzf --ansi \
|
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
--disabled --query "$INITIAL_QUERY" \
|
||||||
--disabled --query "$INITIAL_QUERY" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
|
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
|
||||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
|
--bind "start:unbind(ctrl-r)" \
|
||||||
--prompt '1. Ripgrep> ' \
|
--prompt '1. ripgrep> ' \
|
||||||
--delimiter : \
|
--delimiter : \
|
||||||
--header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||||
)
|
--bind 'enter:become(vim {1} +{2})'
|
||||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- To restore the query string when switching between modes, we store the
|
||||||
|
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
|
||||||
|
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
|
||||||
|
- Also note that we unbind `ctrl-r` binding on `start` event which is
|
||||||
|
triggered once when fzf starts.
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
Log tailing
|
Log tailing
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,6 +1,96 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.40.0
|
||||||
|
------
|
||||||
|
- Added `zero` event that is triggered when there's no match
|
||||||
|
```sh
|
||||||
|
# Reload the candidate list when there's no match
|
||||||
|
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
|
||||||
|
```
|
||||||
|
- New actions
|
||||||
|
- Added `track` action which makes fzf track the current item when the
|
||||||
|
search result is updated. If the user manually moves the cursor, or the
|
||||||
|
item is not in the updated search result, tracking is automatically
|
||||||
|
disabled. Tracking is useful when you want to see the surrounding items
|
||||||
|
by deleting the query string.
|
||||||
|
```sh
|
||||||
|
# Narrow down the list with a query, point to a command,
|
||||||
|
# and hit CTRL-T to see its surrounding commands.
|
||||||
|
export FZF_CTRL_R_OPTS="
|
||||||
|
--preview 'echo {}' --preview-window up:3:hidden:wrap
|
||||||
|
--bind 'ctrl-/:toggle-preview'
|
||||||
|
--bind 'ctrl-t:track+clear-query'
|
||||||
|
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
|
||||||
|
--color header:italic
|
||||||
|
--header 'Press CTRL-Y to copy command into clipboard'"
|
||||||
|
```
|
||||||
|
- Added `change-header(...)`
|
||||||
|
- Added `transform-header(...)`
|
||||||
|
- Added `toggle-track` action
|
||||||
|
- Fixed `--track` behavior when used with `--tac`
|
||||||
|
- However, using `--track` with `--tac` is not recommended. The resulting
|
||||||
|
behavior can be very confusing.
|
||||||
|
- Bug fixes and improvements
|
||||||
|
|
||||||
|
0.39.0
|
||||||
|
------
|
||||||
|
- Added `one` event that is triggered when there's only one match
|
||||||
|
```sh
|
||||||
|
# Automatically select the only match
|
||||||
|
seq 10 | fzf --bind one:accept
|
||||||
|
```
|
||||||
|
- Added `--track` option that makes fzf track the current selection when the
|
||||||
|
result list is updated. This can be useful when browsing logs using fzf with
|
||||||
|
sorting disabled.
|
||||||
|
```sh
|
||||||
|
git log --oneline --graph --color=always | nl |
|
||||||
|
fzf --ansi --track --no-sort --layout=reverse-list
|
||||||
|
```
|
||||||
|
- If you use `--listen` option without a port number fzf will automatically
|
||||||
|
allocate an available port and export it as `$FZF_PORT` environment
|
||||||
|
variable.
|
||||||
|
```sh
|
||||||
|
# Automatic port assignment
|
||||||
|
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||||
|
|
||||||
|
# Say hello
|
||||||
|
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
|
||||||
|
```
|
||||||
|
- A carriage return and a line feed character will be rendered as dim ␍ and
|
||||||
|
␊ respectively.
|
||||||
|
```sh
|
||||||
|
printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}'
|
||||||
|
```
|
||||||
|
- fzf will stop rendering a non-displayable characters as a space. This will
|
||||||
|
likely cause less glitches in the preview window.
|
||||||
|
```sh
|
||||||
|
fzf --preview 'head -1000 /dev/random'
|
||||||
|
```
|
||||||
|
- Bug fixes and improvements
|
||||||
|
|
||||||
|
0.38.0
|
||||||
|
------
|
||||||
|
- New actions
|
||||||
|
- `become(...)` - Replace the current fzf process with the specified
|
||||||
|
command using `execve(2)` system call.
|
||||||
|
See https://github.com/junegunn/fzf#turning-into-a-different-process for
|
||||||
|
more information.
|
||||||
|
```sh
|
||||||
|
# Open selected files in Vim
|
||||||
|
fzf --multi --bind 'enter:become(vim {+})'
|
||||||
|
|
||||||
|
# Open the file in Vim and go to the line
|
||||||
|
git grep --line-number . |
|
||||||
|
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
|
||||||
|
```
|
||||||
|
- This action is not supported on Windows
|
||||||
|
- `show-preview`
|
||||||
|
- `hide-preview`
|
||||||
|
- Bug fixes
|
||||||
|
- `--preview-window 0,hidden` should not execute the preview command until
|
||||||
|
`toggle-preview` action is triggered
|
||||||
|
|
||||||
0.37.0
|
0.37.0
|
||||||
------
|
------
|
||||||
- Added a way to customize the separator of inline info
|
- Added a way to customize the separator of inline info
|
||||||
@@ -118,7 +208,7 @@ CHANGELOG
|
|||||||
- Added color name `preview-label` for `--preview-label` (defaults to `label`
|
- Added color name `preview-label` for `--preview-label` (defaults to `label`
|
||||||
for `--border-label`)
|
for `--border-label`)
|
||||||
- Better support for (Windows) terminals where each box-drawing character
|
- Better support for (Windows) terminals where each box-drawing character
|
||||||
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
|
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
|
||||||
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
|
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
|
||||||
- Behavior changes
|
- Behavior changes
|
||||||
- fzf will always execute the preview command if the command template
|
- fzf will always execute the preview command if the command template
|
||||||
|
|||||||
7
Makefile
7
Makefile
@@ -20,7 +20,7 @@ VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
|||||||
ifdef FZF_REVISION
|
ifdef FZF_REVISION
|
||||||
REVISION := $(FZF_REVISION)
|
REVISION := $(FZF_REVISION)
|
||||||
else
|
else
|
||||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null)
|
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
|
||||||
endif
|
endif
|
||||||
ifeq ($(REVISION),)
|
ifeq ($(REVISION),)
|
||||||
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
||||||
@@ -29,6 +29,7 @@ BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision
|
|||||||
|
|
||||||
BINARY32 := fzf-$(GOOS)_386
|
BINARY32 := fzf-$(GOOS)_386
|
||||||
BINARY64 := fzf-$(GOOS)_amd64
|
BINARY64 := fzf-$(GOOS)_amd64
|
||||||
|
BINARYS390 := fzf-$(GOOS)_s390x
|
||||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||||
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
|
|||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
else ifeq ($(UNAME_M),amd64)
|
else ifeq ($(UNAME_M),amd64)
|
||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
|
else ifeq ($(UNAME_M),s390x)
|
||||||
|
BINARY := $(BINARYS390)
|
||||||
else ifeq ($(UNAME_M),i686)
|
else ifeq ($(UNAME_M),i686)
|
||||||
BINARY := $(BINARY32)
|
BINARY := $(BINARY32)
|
||||||
else ifeq ($(UNAME_M),i386)
|
else ifeq ($(UNAME_M),i386)
|
||||||
@@ -132,6 +135,8 @@ target/$(BINARY32): $(SOURCES)
|
|||||||
target/$(BINARY64): $(SOURCES)
|
target/$(BINARY64): $(SOURCES)
|
||||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
|
target/$(BINARYS390): $(SOURCES)
|
||||||
|
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
# https://github.com/golang/go/wiki/GoArm
|
# https://github.com/golang/go/wiki/GoArm
|
||||||
target/$(BINARYARM5): $(SOURCES)
|
target/$(BINARYARM5): $(SOURCES)
|
||||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ set rtp+=/usr/local/opt/fzf
|
|||||||
" If installed using Homebrew on Apple Silicon
|
" If installed using Homebrew on Apple Silicon
|
||||||
set rtp+=/opt/homebrew/opt/fzf
|
set rtp+=/opt/homebrew/opt/fzf
|
||||||
|
|
||||||
" If installed using git
|
" If you have cloned fzf on ~/.fzf directory
|
||||||
set rtp+=~/.fzf
|
set rtp+=~/.fzf
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ written as:
|
|||||||
" If installed using Homebrew
|
" If installed using Homebrew
|
||||||
Plug '/usr/local/opt/fzf'
|
Plug '/usr/local/opt/fzf'
|
||||||
|
|
||||||
" If installed using git
|
" If you have cloned fzf on ~/.fzf directory
|
||||||
Plug '~/.fzf'
|
Plug '~/.fzf'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ let g:fzf_action = {
|
|||||||
|
|
||||||
" An action can be a reference to a function that processes selected lines
|
" An action can be a reference to a function that processes selected lines
|
||||||
function! s:build_quickfix_list(lines)
|
function! s:build_quickfix_list(lines)
|
||||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||||
copen
|
copen
|
||||||
cc
|
cc
|
||||||
endfunction
|
endfunction
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -54,14 +54,15 @@ Table of Contents
|
|||||||
* [Advanced topics](#advanced-topics)
|
* [Advanced topics](#advanced-topics)
|
||||||
* [Performance](#performance)
|
* [Performance](#performance)
|
||||||
* [Executing external programs](#executing-external-programs)
|
* [Executing external programs](#executing-external-programs)
|
||||||
|
* [Turning into a different process](#turning-into-a-different-process)
|
||||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||||
* [Preview window](#preview-window)
|
* [Preview window](#preview-window)
|
||||||
* [Tips](#tips)
|
* [Tips](#tips)
|
||||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||||
* [Fish shell](#fish-shell)
|
* [Fish shell](#fish-shell)
|
||||||
* [Related projects](#related-projects)
|
* [Related projects](#related-projects)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
|||||||
| pkg | FreeBSD | `pkg install fzf` |
|
| pkg | FreeBSD | `pkg install fzf` |
|
||||||
| pkgin | NetBSD | `pkgin install fzf` |
|
| pkgin | NetBSD | `pkgin install fzf` |
|
||||||
| pkg_add | OpenBSD | `pkg_add fzf` |
|
| pkg_add | OpenBSD | `pkg_add fzf` |
|
||||||
|
| Portage | Gentoo | `emerge --ask app-shells/fzf` |
|
||||||
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
||||||
| Zypper | openSUSE | `sudo zypper install fzf` |
|
| Zypper | openSUSE | `sudo zypper install fzf` |
|
||||||
|
|
||||||
@@ -136,15 +138,17 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
|||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
||||||
available via [Chocolatey][choco] and [Scoop][scoop]:
|
available via [Chocolatey][choco], [Scoop][scoop], and [Winget][winget]:
|
||||||
|
|
||||||
| Package manager | Command |
|
| Package manager | Command |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Chocolatey | `choco install fzf` |
|
| Chocolatey | `choco install fzf` |
|
||||||
| Scoop | `scoop install fzf` |
|
| Scoop | `scoop install fzf` |
|
||||||
|
| Winget | `winget install fzf` |
|
||||||
|
|
||||||
[choco]: https://chocolatey.org/packages/fzf
|
[choco]: https://chocolatey.org/packages/fzf
|
||||||
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
|
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
|
||||||
|
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/j/junegunn/fzf
|
||||||
|
|
||||||
Known issues and limitations on Windows can be found on [the wiki
|
Known issues and limitations on Windows can be found on [the wiki
|
||||||
page][windows-wiki].
|
page][windows-wiki].
|
||||||
@@ -202,6 +206,22 @@ files excluding hidden ones. (You can override the default command with
|
|||||||
vim $(fzf)
|
vim $(fzf)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> *:bulb: A more robust solution would be to use `xargs` but we've presented
|
||||||
|
> the above as it's easier to grasp*
|
||||||
|
> ```sh
|
||||||
|
> fzf --print0 | xargs -0 -o vim
|
||||||
|
> ```
|
||||||
|
|
||||||
|
>
|
||||||
|
> *:bulb: fzf also has the ability to turn itself into a different process.*
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> fzf --bind 'enter:become(vim {})'
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> *See [Turning into a different process](#turning-into-a-different-process)
|
||||||
|
> for more information.*
|
||||||
|
|
||||||
### Using the finder
|
### Using the finder
|
||||||
|
|
||||||
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
- `CTRL-K` / `CTRL-J` (or `CTRL-P` / `CTRL-N`) to move cursor up and down
|
||||||
@@ -560,6 +580,47 @@ fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort
|
|||||||
|
|
||||||
See *KEY BINDINGS* section of the man page for details.
|
See *KEY BINDINGS* section of the man page for details.
|
||||||
|
|
||||||
|
### Turning into a different process
|
||||||
|
|
||||||
|
`become(...)` is similar to `execute(...)`/`execute-silent(...)` described
|
||||||
|
above, but instead of executing the command and coming back to fzf on
|
||||||
|
complete, it turns fzf into a new process for the command.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --bind 'enter:become(vim {})'
|
||||||
|
```
|
||||||
|
|
||||||
|
Compared to the seemingly equivalent command substitution `vim "$(fzf)"`, this
|
||||||
|
approach has several advantages:
|
||||||
|
|
||||||
|
* Vim will not open an empty file when you terminate fzf with
|
||||||
|
<kbd>CTRL-C</kbd>
|
||||||
|
* Vim will not open an empty file when you press <kbd>ENTER</kbd> on an empty
|
||||||
|
result
|
||||||
|
* Can handle multiple selections even when they have whitespaces
|
||||||
|
```sh
|
||||||
|
fzf --multi --bind 'enter:become(vim {+})'
|
||||||
|
```
|
||||||
|
|
||||||
|
To be fair, running `fzf --print0 | xargs -0 -o vim` instead of `vim "$(fzf)"`
|
||||||
|
resolves all of the issues mentioned. Nonetheless, `become(...)` still offers
|
||||||
|
additional benefits in different scenarios.
|
||||||
|
|
||||||
|
* You can set up multiple bindings to handle the result in different ways
|
||||||
|
without any wrapping script
|
||||||
|
```sh
|
||||||
|
fzf --bind 'enter:become(vim {}),ctrl-e:become(emacs {})'
|
||||||
|
```
|
||||||
|
* Previously, you would have to use `--expect=ctrl-e` and check the first
|
||||||
|
line of the output of fzf
|
||||||
|
* You can easily build the subsequent command using the field index
|
||||||
|
expressions of fzf
|
||||||
|
```sh
|
||||||
|
# Open the file in Vim and go to the line
|
||||||
|
git grep --line-number . |
|
||||||
|
fzf --delimiter : --nth 3.. --bind 'enter:become(vim {1} +{2})'
|
||||||
|
```
|
||||||
|
|
||||||
### Reloading the candidate list
|
### Reloading the candidate list
|
||||||
|
|
||||||
By binding `reload` action to a key or an event, you can make fzf dynamically
|
By binding `reload` action to a key or an event, you can make fzf dynamically
|
||||||
@@ -663,7 +724,7 @@ history | fzf
|
|||||||
Tips
|
Tips
|
||||||
----
|
----
|
||||||
|
|
||||||
#### Respecting `.gitignore`
|
### Respecting `.gitignore`
|
||||||
|
|
||||||
You can use [fd](https://github.com/sharkdp/fd),
|
You can use [fd](https://github.com/sharkdp/fd),
|
||||||
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
|
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
|
||||||
@@ -692,7 +753,7 @@ hidden files, use the following command:
|
|||||||
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
|
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix --hidden --follow --exclude .git'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Fish shell
|
### Fish shell
|
||||||
|
|
||||||
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
|
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
|
||||||
token on the command-line as the root directory for the recursive search. For
|
token on the command-line as the root directory for the recursive search. For
|
||||||
|
|||||||
10
bin/fzf-tmux
10
bin/fzf-tmux
@@ -179,11 +179,15 @@ trap 'cleanup' EXIT
|
|||||||
|
|
||||||
envs="export TERM=$TERM "
|
envs="export TERM=$TERM "
|
||||||
if [[ "$opt" =~ "-E" ]]; then
|
if [[ "$opt" =~ "-E" ]]; then
|
||||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||||
tmux_version=$(tmux -V)
|
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||||
if [[ ! $tmux_version =~ 3\.2 ]]; then
|
|
||||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||||
opt="-B $opt"
|
opt="-B $opt"
|
||||||
|
elif [[ $tmux_version = 3.2 ]]; then
|
||||||
|
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||||
|
else
|
||||||
|
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||||
|
exit 2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
fzf.txt fzf Last change: May 19 2021
|
fzf.txt fzf Last change: Mar 20 2023
|
||||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ depending on the package manager.
|
|||||||
" If installed using Homebrew
|
" If installed using Homebrew
|
||||||
set rtp+=/usr/local/opt/fzf
|
set rtp+=/usr/local/opt/fzf
|
||||||
|
|
||||||
" If installed using git
|
" If you have cloned fzf on ~/.fzf directory
|
||||||
set rtp+=~/.fzf
|
set rtp+=~/.fzf
|
||||||
<
|
<
|
||||||
If you use {vim-plug}{1}, the same can be written as:
|
If you use {vim-plug}{1}, the same can be written as:
|
||||||
@@ -40,7 +40,7 @@ If you use {vim-plug}{1}, the same can be written as:
|
|||||||
" If installed using Homebrew
|
" If installed using Homebrew
|
||||||
Plug '/usr/local/opt/fzf'
|
Plug '/usr/local/opt/fzf'
|
||||||
|
|
||||||
" If installed using git
|
" If you have cloned fzf on ~/.fzf directory
|
||||||
Plug '~/.fzf'
|
Plug '~/.fzf'
|
||||||
<
|
<
|
||||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||||
@@ -143,7 +143,7 @@ Examples~
|
|||||||
|
|
||||||
" An action can be a reference to a function that processes selected lines
|
" An action can be a reference to a function that processes selected lines
|
||||||
function! s:build_quickfix_list(lines)
|
function! s:build_quickfix_list(lines)
|
||||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||||
copen
|
copen
|
||||||
cc
|
cc
|
||||||
endfunction
|
endfunction
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -5,10 +5,10 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.17
|
github.com/mattn/go-isatty v0.0.17
|
||||||
github.com/mattn/go-runewidth v0.0.14
|
github.com/mattn/go-runewidth v0.0.14
|
||||||
github.com/mattn/go-shellwords v1.0.12
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
github.com/rivo/uniseg v0.4.2
|
github.com/rivo/uniseg v0.4.4
|
||||||
github.com/saracen/walker v0.1.3
|
github.com/saracen/walker v0.1.3
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
golang.org/x/sys v0.7.0
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
golang.org/x/term v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -11,8 +11,8 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
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/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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||||
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -31,11 +31,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||||
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|||||||
3
install
3
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.37.0
|
version=0.40.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
@@ -178,6 +178,7 @@ case "$archi" in
|
|||||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||||
|
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.37.0"
|
$version="0.40.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version string = "0.37"
|
var version string = "0.40"
|
||||||
var revision string = "devel"
|
var revision string = "devel"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf-tmux 1 "Jan 2023" "fzf 0.37.0" "fzf-tmux - open fzf in tmux split pane"
|
.TH fzf-tmux 1 "May 2023" "fzf 0.40.0" "fzf-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf-tmux - open fzf in tmux split pane
|
fzf-tmux - open fzf in tmux split pane
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf 1 "Jan 2023" "fzf 0.37.0" "fzf - a command-line fuzzy finder"
|
.TH fzf 1 "May 2023" "fzf 0.40.0" "fzf - a command-line fuzzy finder"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf - a command-line fuzzy finder
|
fzf - a command-line fuzzy finder
|
||||||
@@ -92,6 +92,19 @@ interface rather than a "fuzzy finder". You can later enable the search using
|
|||||||
.B "+s, --no-sort"
|
.B "+s, --no-sort"
|
||||||
Do not sort the result
|
Do not sort the result
|
||||||
.TP
|
.TP
|
||||||
|
.B "--track"
|
||||||
|
Make fzf track the current selection when the result list is updated.
|
||||||
|
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||||
|
not recommended to use this option with \fB--tac\fR as the resulting behavior
|
||||||
|
can be confusing. Also, consider using \fBtrack\fR action instead of this
|
||||||
|
option.
|
||||||
|
|
||||||
|
.RS
|
||||||
|
e.g.
|
||||||
|
\fBgit log --oneline --graph --color=always | nl |
|
||||||
|
fzf --ansi --track --no-sort --layout=reverse-list\fR
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B "--tac"
|
.B "--tac"
|
||||||
Reverse the order of the input
|
Reverse the order of the input
|
||||||
|
|
||||||
@@ -231,8 +244,9 @@ Draw border around the finder
|
|||||||
.br
|
.br
|
||||||
|
|
||||||
If you use a terminal emulator where each box-drawing character takes
|
If you use a terminal emulator where each box-drawing character takes
|
||||||
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
|
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
|
||||||
still not properly rendered, set \fB--no-unicode\fR.
|
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
|
||||||
|
\fB--no-unicode\fR.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "--border-label" [=LABEL]
|
.BI "--border-label" [=LABEL]
|
||||||
@@ -738,9 +752,12 @@ ncurses finder only after the input stream is complete.
|
|||||||
e.g. \fBfzf --multi | fzf --sync\fR
|
e.g. \fBfzf --multi | fzf --sync\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.B "--listen=HTTP_PORT"
|
.B "--listen[=HTTP_PORT]"
|
||||||
Start HTTP server on the given port. It allows external processes to send
|
Start HTTP server on the given port. It allows external processes to send
|
||||||
actions to perform via POST method.
|
actions to perform via POST method. If the port number is omitted or given as
|
||||||
|
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
|
||||||
|
environment variable to the child processes started via \fBexecute\fR and
|
||||||
|
\fBexecute-silent\fR actions.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
\fB# Start HTTP server on port 6266
|
\fB# Start HTTP server on port 6266
|
||||||
@@ -748,6 +765,9 @@ e.g.
|
|||||||
|
|
||||||
# Send action to the server
|
# Send action to the server
|
||||||
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
|
||||||
|
|
||||||
|
# Choose port automatically and export it as $FZF_PORT to the child process
|
||||||
|
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
|
||||||
\fR
|
\fR
|
||||||
.TP
|
.TP
|
||||||
.B "--version"
|
.B "--version"
|
||||||
@@ -977,6 +997,28 @@ e.g.
|
|||||||
# Beware not to introduce an infinite loop
|
# Beware not to introduce an infinite loop
|
||||||
seq 10 | fzf --bind 'focus:up' --cycle\fR
|
seq 10 | fzf --bind 'focus:up' --cycle\fR
|
||||||
.RE
|
.RE
|
||||||
|
\fIone\fR
|
||||||
|
.RS
|
||||||
|
Triggered when there's only one match. \fBone:accept\fR binding is comparable
|
||||||
|
to \fB--select-1\fR option, but the difference is that \fB--select-1\fR is only
|
||||||
|
effective before the interactive finder starts but \fBone\fR event is triggered
|
||||||
|
by the interactive finder.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
\fB# Automatically select the only match
|
||||||
|
seq 10 | fzf --bind one:accept\fR
|
||||||
|
.RE
|
||||||
|
\fIzero\fR
|
||||||
|
.RS
|
||||||
|
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
|
||||||
|
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
|
||||||
|
effective before the interactive finder starts but \fBzero\fR event is
|
||||||
|
triggered by the interactive finder.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
\fB# Reload the candidate list when there's no match
|
||||||
|
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
|
||||||
|
.RE
|
||||||
|
|
||||||
\fIbackward-eof\fR
|
\fIbackward-eof\fR
|
||||||
.RS
|
.RS
|
||||||
@@ -999,9 +1041,11 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||||
\fBbackward-kill-word\fR \fIalt-bs\fR
|
\fBbackward-kill-word\fR \fIalt-bs\fR
|
||||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||||
|
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||||
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
|
||||||
|
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
|
||||||
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
\fBchange-preview(...)\fR (change \fB--preview\fR option)
|
||||||
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
|
||||||
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
|
||||||
@@ -1036,6 +1080,7 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBpage-up\fR \fIpgup\fR
|
\fBpage-up\fR \fIpgup\fR
|
||||||
\fBhalf-page-down\fR
|
\fBhalf-page-down\fR
|
||||||
\fBhalf-page-up\fR
|
\fBhalf-page-up\fR
|
||||||
|
\fBhide-preview\fR
|
||||||
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
||||||
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
\fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||||
\fBprev-selected\fR (move to the previous selected item)
|
\fBprev-selected\fR (move to the previous selected item)
|
||||||
@@ -1058,6 +1103,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)
|
\fBreplace-query\fR (replace query string with the current selection)
|
||||||
\fBselect\fR
|
\fBselect\fR
|
||||||
\fBselect-all\fR (select all matches)
|
\fBselect-all\fR (select all matches)
|
||||||
|
\fBshow-preview\fR
|
||||||
\fBtoggle\fR (\fIright-click\fR)
|
\fBtoggle\fR (\fIright-click\fR)
|
||||||
\fBtoggle-all\fR (toggle all matches)
|
\fBtoggle-all\fR (toggle all matches)
|
||||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||||
@@ -1067,8 +1113,11 @@ A key or an event can be bound to one or more of the following actions.
|
|||||||
\fBtoggle-preview-wrap\fR
|
\fBtoggle-preview-wrap\fR
|
||||||
\fBtoggle-search\fR (toggle search functionality)
|
\fBtoggle-search\fR (toggle search functionality)
|
||||||
\fBtoggle-sort\fR
|
\fBtoggle-sort\fR
|
||||||
|
\fBtoggle-track\fR
|
||||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||||
|
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||||
\fBtransform-border-label(...)\fR (transform border label using 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)
|
\fBtransform-preview-label(...)\fR (transform preview label using an external command)
|
||||||
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
|
\fBtransform-prompt(...)\fR (transform prompt string using an external command)
|
||||||
\fBtransform-query(...)\fR (transform query string using an external command)
|
\fBtransform-query(...)\fR (transform query string using an external command)
|
||||||
@@ -1141,6 +1190,14 @@ On *nix systems, fzf runs the command 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
|
set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
|
||||||
POSIX-compliant.
|
POSIX-compliant.
|
||||||
|
|
||||||
|
\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the
|
||||||
|
current fzf process with the specified command using \fBexecve(2)\fR system
|
||||||
|
call.
|
||||||
|
|
||||||
|
\fBfzf --bind "enter:become(vim {})"\fR
|
||||||
|
|
||||||
|
\fBbecome(...)\fR is not supported on Windows.
|
||||||
|
|
||||||
.SS RELOAD INPUT
|
.SS RELOAD INPUT
|
||||||
|
|
||||||
\fBreload(...)\fR action is used to dynamically update the input list
|
\fBreload(...)\fR action is used to dynamically update the input list
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function s:get_version(bin)
|
|||||||
if has_key(s:versions, a:bin)
|
if has_key(s:versions, a:bin)
|
||||||
return s:versions[a:bin]
|
return s:versions[a:bin]
|
||||||
end
|
end
|
||||||
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||||
let output = systemlist(command)
|
let output = systemlist(command)
|
||||||
if v:shell_error || empty(output)
|
if v:shell_error || empty(output)
|
||||||
return ''
|
return ''
|
||||||
@@ -512,9 +512,7 @@ try
|
|||||||
let optstr .= ' --height='.height
|
let optstr .= ' --height='.height
|
||||||
endif
|
endif
|
||||||
" Respect --border option given in 'options'
|
" Respect --border option given in 'options'
|
||||||
if stridx(optstr, '--border') < 0 && stridx(optstr, '--no-border') < 0
|
let optstr = join([s:border_opt(get(dict, 'window', 0)), optstr])
|
||||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
|
||||||
endif
|
|
||||||
let prev_default_command = $FZF_DEFAULT_COMMAND
|
let prev_default_command = $FZF_DEFAULT_COMMAND
|
||||||
if len(source_command)
|
if len(source_command)
|
||||||
let $FZF_DEFAULT_COMMAND = source_command
|
let $FZF_DEFAULT_COMMAND = source_command
|
||||||
@@ -741,7 +739,7 @@ function! s:calc_size(max, val, dict)
|
|||||||
return size
|
return size
|
||||||
endif
|
endif
|
||||||
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
||||||
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
|
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
|
||||||
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||||
let margin += len(split(opts, "\n"))
|
let margin += len(split(opts, "\n"))
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -270,8 +270,9 @@ _fzf_complete_kill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_proc_completion() {
|
_fzf_proc_completion() {
|
||||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||||
command ps -ef | sed 1d
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
|
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +310,7 @@ complete -o default -F _fzf_opts_completion fzf-tmux
|
|||||||
|
|
||||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||||
a_cmds="
|
a_cmds="
|
||||||
awk cat diff diff3
|
awk bat cat diff diff3
|
||||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||||
javac ld less more mvim nvim patch perl python ruby
|
javac ld less more mvim nvim patch perl python ruby
|
||||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||||
@@ -373,7 +374,7 @@ _fzf_setup_completion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Environment variables / Aliases / Hosts / Process
|
# Environment variables / Aliases / Hosts / Process
|
||||||
_fzf_setup_completion 'var' export unset
|
_fzf_setup_completion 'var' export unset printenv
|
||||||
_fzf_setup_completion 'alias' unalias
|
_fzf_setup_completion 'alias' unalias
|
||||||
_fzf_setup_completion 'host' ssh telnet
|
_fzf_setup_completion 'host' ssh telnet
|
||||||
_fzf_setup_completion 'proc' kill
|
_fzf_setup_completion 'proc' kill
|
||||||
|
|||||||
@@ -251,8 +251,9 @@ _fzf_complete_unalias() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_kill() {
|
_fzf_complete_kill() {
|
||||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||||
command ps -ef | sed 1d
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
|
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/core.go
12
src/core.go
@@ -219,6 +219,7 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
determine := func(final bool) {
|
determine := func(final bool) {
|
||||||
if heightUnknown {
|
if heightUnknown {
|
||||||
if total >= maxFit || final {
|
if total >= maxFit || final {
|
||||||
|
deferred = false
|
||||||
heightUnknown = false
|
heightUnknown = false
|
||||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
||||||
}
|
}
|
||||||
@@ -298,10 +299,12 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
|
|
||||||
case EvtSearchNew:
|
case EvtSearchNew:
|
||||||
var command *string
|
var command *string
|
||||||
|
var changed bool
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case searchRequest:
|
case searchRequest:
|
||||||
sort = val.sort
|
sort = val.sort
|
||||||
command = val.command
|
command = val.command
|
||||||
|
changed = val.changed
|
||||||
if command != nil {
|
if command != nil {
|
||||||
useSnapshot = val.sync
|
useSnapshot = val.sync
|
||||||
}
|
}
|
||||||
@@ -313,10 +316,17 @@ func Run(opts *Options, version string, revision string) {
|
|||||||
} else {
|
} else {
|
||||||
restart(*command)
|
restart(*command)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !useSnapshot {
|
if !useSnapshot {
|
||||||
snapshot, _ = chunkList.Snapshot()
|
newSnapshot, _ := chunkList.Snapshot()
|
||||||
|
// We want to avoid showing empty list when reload is triggered
|
||||||
|
// and the query string is changed at the same time i.e. command != nil && changed
|
||||||
|
if command == nil || len(newSnapshot) > 0 {
|
||||||
|
snapshot = newSnapshot
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reset := !useSnapshot && clearCache()
|
reset := !useSnapshot && clearCache()
|
||||||
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Merger struct {
|
|||||||
tac bool
|
tac bool
|
||||||
final bool
|
final bool
|
||||||
count int
|
count int
|
||||||
|
pass bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassMerger returns a new Merger that simply returns the items in the
|
// PassMerger returns a new Merger that simply returns the items in the
|
||||||
@@ -26,7 +27,8 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
|||||||
pattern: nil,
|
pattern: nil,
|
||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
count: 0}
|
count: 0,
|
||||||
|
pass: true}
|
||||||
|
|
||||||
for _, chunk := range *mg.chunks {
|
for _, chunk := range *mg.chunks {
|
||||||
mg.count += chunk.count
|
mg.count += chunk.count
|
||||||
@@ -58,6 +60,32 @@ func (mg *Merger) Length() int {
|
|||||||
return mg.count
|
return mg.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mg *Merger) First() Result {
|
||||||
|
if mg.tac && !mg.sorted {
|
||||||
|
return mg.Get(mg.count - 1)
|
||||||
|
}
|
||||||
|
return mg.Get(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindIndex returns the index of the item with the given item index
|
||||||
|
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||||
|
index := -1
|
||||||
|
if mg.pass {
|
||||||
|
index = int(itemIndex)
|
||||||
|
if mg.tac {
|
||||||
|
index = mg.count - index - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := 0; i < mg.count; i++ {
|
||||||
|
if mg.Get(i).item.Index() == itemIndex {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns the pointer to the Result object indexed by the given integer
|
// Get returns the pointer to the Result object indexed by the given integer
|
||||||
func (mg *Merger) Get(idx int) Result {
|
func (mg *Merger) Get(idx int) Result {
|
||||||
if mg.chunks != nil {
|
if mg.chunks != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/junegunn/fzf/src/algo"
|
"github.com/junegunn/fzf/src/algo"
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
@@ -32,6 +33,7 @@ const usage = `usage: fzf [options]
|
|||||||
field index expressions
|
field index expressions
|
||||||
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
-d, --delimiter=STR Field delimiter regex (default: AWK-style)
|
||||||
+s, --no-sort Do not sort the result
|
+s, --no-sort Do not sort the result
|
||||||
|
--track Track the current selection when the result is updated
|
||||||
--tac Reverse the order of the input
|
--tac Reverse the order of the input
|
||||||
--disabled Do not perform search
|
--disabled Do not perform search
|
||||||
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
--tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
|
||||||
@@ -115,7 +117,7 @@ const usage = `usage: fzf [options]
|
|||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
--print0 Print output delimited by ASCII NUL characters
|
--print0 Print output delimited by ASCII NUL characters
|
||||||
--sync Synchronous search for multi-staged filtering
|
--sync Synchronous search for multi-staged filtering
|
||||||
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
|
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /)
|
||||||
--version Display version information and exit
|
--version Display version information and exit
|
||||||
|
|
||||||
Environment variables
|
Environment variables
|
||||||
@@ -163,6 +165,14 @@ func defaultMargin() [4]sizeSpec {
|
|||||||
return [4]sizeSpec{}
|
return [4]sizeSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type trackOption int
|
||||||
|
|
||||||
|
const (
|
||||||
|
trackDisabled trackOption = iota
|
||||||
|
trackEnabled
|
||||||
|
trackCurrent
|
||||||
|
)
|
||||||
|
|
||||||
type windowPosition int
|
type windowPosition int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -265,6 +275,7 @@ type Options struct {
|
|||||||
WithNth []Range
|
WithNth []Range
|
||||||
Delimiter Delimiter
|
Delimiter Delimiter
|
||||||
Sort int
|
Sort int
|
||||||
|
Track trackOption
|
||||||
Tac bool
|
Tac bool
|
||||||
Criteria []criterion
|
Criteria []criterion
|
||||||
Multi int
|
Multi int
|
||||||
@@ -315,7 +326,7 @@ type Options struct {
|
|||||||
PreviewLabel labelOpts
|
PreviewLabel labelOpts
|
||||||
Unicode bool
|
Unicode bool
|
||||||
Tabstop int
|
Tabstop int
|
||||||
ListenPort int
|
ListenPort *int
|
||||||
ClearOnExit bool
|
ClearOnExit bool
|
||||||
Version bool
|
Version bool
|
||||||
}
|
}
|
||||||
@@ -337,6 +348,7 @@ func defaultOptions() *Options {
|
|||||||
WithNth: make([]Range, 0),
|
WithNth: make([]Range, 0),
|
||||||
Delimiter: Delimiter{},
|
Delimiter: Delimiter{},
|
||||||
Sort: 1000,
|
Sort: 1000,
|
||||||
|
Track: trackDisabled,
|
||||||
Tac: false,
|
Tac: false,
|
||||||
Criteria: []criterion{byScore, byLength},
|
Criteria: []criterion{byScore, byLength},
|
||||||
Multi: 0,
|
Multi: 0,
|
||||||
@@ -618,6 +630,10 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
|||||||
add(tui.Load)
|
add(tui.Load)
|
||||||
case "focus":
|
case "focus":
|
||||||
add(tui.Focus)
|
add(tui.Focus)
|
||||||
|
case "one":
|
||||||
|
add(tui.One)
|
||||||
|
case "zero":
|
||||||
|
add(tui.Zero)
|
||||||
case "alt-enter", "alt-return":
|
case "alt-enter", "alt-return":
|
||||||
chords[tui.CtrlAltKey('m')] = key
|
chords[tui.CtrlAltKey('m')] = key
|
||||||
case "alt-space":
|
case "alt-space":
|
||||||
@@ -921,7 +937,7 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
executeRegexp = regexp.MustCompile(
|
executeRegexp = regexp.MustCompile(
|
||||||
`(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?: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)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||||
splitRegexp = regexp.MustCompile("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1093,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actToggleAll)
|
appendAction(actToggleAll)
|
||||||
case "toggle-search":
|
case "toggle-search":
|
||||||
appendAction(actToggleSearch)
|
appendAction(actToggleSearch)
|
||||||
|
case "toggle-track":
|
||||||
|
appendAction(actToggleTrack)
|
||||||
|
case "track":
|
||||||
|
appendAction(actTrack)
|
||||||
case "select":
|
case "select":
|
||||||
appendAction(actSelect)
|
appendAction(actSelect)
|
||||||
case "select-all":
|
case "select-all":
|
||||||
@@ -1111,6 +1131,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
appendAction(actPrevSelected)
|
appendAction(actPrevSelected)
|
||||||
case "next-selected":
|
case "next-selected":
|
||||||
appendAction(actNextSelected)
|
appendAction(actNextSelected)
|
||||||
|
case "show-preview":
|
||||||
|
appendAction(actShowPreview)
|
||||||
|
case "hide-preview":
|
||||||
|
appendAction(actHidePreview)
|
||||||
case "toggle-preview":
|
case "toggle-preview":
|
||||||
appendAction(actTogglePreview)
|
appendAction(actTogglePreview)
|
||||||
case "toggle-preview-wrap":
|
case "toggle-preview-wrap":
|
||||||
@@ -1167,6 +1191,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
|||||||
actions = append(actions, &action{t: t, a: actionArg})
|
actions = append(actions, &action{t: t, a: actionArg})
|
||||||
}
|
}
|
||||||
switch t {
|
switch t {
|
||||||
|
case actBecome:
|
||||||
|
if util.IsWindows() {
|
||||||
|
exit("become action is not supported on Windows")
|
||||||
|
}
|
||||||
case actUnbind, actRebind:
|
case actUnbind, actRebind:
|
||||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||||
case actChangePreviewWindow:
|
case actChangePreviewWindow:
|
||||||
@@ -1219,6 +1247,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
|
|
||||||
prefix := actionNameRegexp.FindString(str)
|
prefix := actionNameRegexp.FindString(str)
|
||||||
switch prefix {
|
switch prefix {
|
||||||
|
case "become":
|
||||||
|
return actBecome
|
||||||
case "reload":
|
case "reload":
|
||||||
return actReload
|
return actReload
|
||||||
case "reload-sync":
|
case "reload-sync":
|
||||||
@@ -1231,6 +1261,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actPreview
|
return actPreview
|
||||||
case "change-border-label":
|
case "change-border-label":
|
||||||
return actChangeBorderLabel
|
return actChangeBorderLabel
|
||||||
|
case "change-header":
|
||||||
|
return actChangeHeader
|
||||||
case "change-preview-label":
|
case "change-preview-label":
|
||||||
return actChangePreviewLabel
|
return actChangePreviewLabel
|
||||||
case "change-preview-window":
|
case "change-preview-window":
|
||||||
@@ -1255,6 +1287,8 @@ func isExecuteAction(str string) actionType {
|
|||||||
return actTransformBorderLabel
|
return actTransformBorderLabel
|
||||||
case "transform-preview-label":
|
case "transform-preview-label":
|
||||||
return actTransformPreviewLabel
|
return actTransformPreviewLabel
|
||||||
|
case "transform-header":
|
||||||
|
return actTransformHeader
|
||||||
case "transform-prompt":
|
case "transform-prompt":
|
||||||
return actTransformPrompt
|
return actTransformPrompt
|
||||||
case "transform-query":
|
case "transform-query":
|
||||||
@@ -1551,6 +1585,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
opts.Sort = optionalNumeric(allArgs, &i, 1)
|
||||||
case "+s", "--no-sort":
|
case "+s", "--no-sort":
|
||||||
opts.Sort = 0
|
opts.Sort = 0
|
||||||
|
case "--track":
|
||||||
|
opts.Track = trackEnabled
|
||||||
|
case "--no-track":
|
||||||
|
opts.Track = trackDisabled
|
||||||
case "--tac":
|
case "--tac":
|
||||||
opts.Tac = true
|
opts.Tac = true
|
||||||
case "--no-tac":
|
case "--no-tac":
|
||||||
@@ -1745,9 +1783,10 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--tabstop":
|
case "--tabstop":
|
||||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||||
case "--listen":
|
case "--listen":
|
||||||
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
|
port := optionalNumeric(allArgs, &i, 0)
|
||||||
|
opts.ListenPort = &port
|
||||||
case "--no-listen":
|
case "--no-listen":
|
||||||
opts.ListenPort = 0
|
opts.ListenPort = nil
|
||||||
case "--clear":
|
case "--clear":
|
||||||
opts.ClearOnExit = true
|
opts.ClearOnExit = true
|
||||||
case "--no-clear":
|
case "--no-clear":
|
||||||
@@ -1838,7 +1877,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
} else if match, value := optString(arg, "--tabstop="); match {
|
} else if match, value := optString(arg, "--tabstop="); match {
|
||||||
opts.Tabstop = atoi(value)
|
opts.Tabstop = atoi(value)
|
||||||
} else if match, value := optString(arg, "--listen="); match {
|
} else if match, value := optString(arg, "--listen="); match {
|
||||||
opts.ListenPort = atoi(value)
|
port := atoi(value)
|
||||||
|
opts.ListenPort = &port
|
||||||
} else if match, value := optString(arg, "--hscroll-off="); match {
|
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||||
opts.HscrollOff = atoi(value)
|
opts.HscrollOff = atoi(value)
|
||||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
} else if match, value := optString(arg, "--scroll-off="); match {
|
||||||
@@ -1868,7 +1908,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
errorExit("tab stop must be a positive integer")
|
errorExit("tab stop must be a positive integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
|
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
|
||||||
errorExit("invalid listen port")
|
errorExit("invalid listen port")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1990,9 +2030,7 @@ func postProcessOptions(opts *Options) {
|
|||||||
theme := opts.Theme
|
theme := opts.Theme
|
||||||
boldify := func(c tui.ColorAttr) tui.ColorAttr {
|
boldify := func(c tui.ColorAttr) tui.ColorAttr {
|
||||||
dup := c
|
dup := c
|
||||||
if !theme.Colored {
|
if (c.Attr & tui.AttrRegular) == 0 {
|
||||||
dup.Attr |= tui.Bold
|
|
||||||
} else if (c.Attr & tui.AttrRegular) == 0 {
|
|
||||||
dup.Attr |= tui.Bold
|
dup.Attr |= tui.Bold
|
||||||
}
|
}
|
||||||
return dup
|
return dup
|
||||||
|
|||||||
@@ -19,14 +19,26 @@ const (
|
|||||||
maxContentLength = 1024 * 1024
|
maxContentLength = 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
func startHttpServer(port int, channel chan []*action) error {
|
func startHttpServer(port int, channel chan []*action) (error, int) {
|
||||||
if port == 0 {
|
if port < 0 {
|
||||||
return nil
|
return nil, port
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("port not available: %d", port)
|
return fmt.Errorf("port not available: %d", port), port
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
addr := listener.Addr().String()
|
||||||
|
parts := strings.SplitN(addr, ":", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
port, err = strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return err, port
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -45,7 +57,7 @@ func startHttpServer(port int, channel chan []*action) error {
|
|||||||
listener.Close()
|
listener.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil, port
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we are writing a simplistic HTTP server without using net/http
|
// Here we are writing a simplistic HTTP server without using net/http
|
||||||
|
|||||||
293
src/terminal.go
293
src/terminal.go
@@ -3,9 +3,11 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -104,7 +106,6 @@ type previewer struct {
|
|||||||
version int64
|
version int64
|
||||||
lines []string
|
lines []string
|
||||||
offset int
|
offset int
|
||||||
enabled bool
|
|
||||||
scrollable bool
|
scrollable bool
|
||||||
final bool
|
final bool
|
||||||
following resumableState
|
following resumableState
|
||||||
@@ -183,6 +184,7 @@ type Terminal struct {
|
|||||||
multi int
|
multi int
|
||||||
sort bool
|
sort bool
|
||||||
toggleSort bool
|
toggleSort bool
|
||||||
|
track trackOption
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
expect map[tui.Event]string
|
expect map[tui.Event]string
|
||||||
keymap map[tui.Event][]*action
|
keymap map[tui.Event][]*action
|
||||||
@@ -201,9 +203,8 @@ type Terminal struct {
|
|||||||
tabstop int
|
tabstop int
|
||||||
margin [4]sizeSpec
|
margin [4]sizeSpec
|
||||||
padding [4]sizeSpec
|
padding [4]sizeSpec
|
||||||
strong tui.Attr
|
|
||||||
unicode bool
|
unicode bool
|
||||||
listenPort int
|
listenPort *int
|
||||||
borderShape tui.BorderShape
|
borderShape tui.BorderShape
|
||||||
cleanExit bool
|
cleanExit bool
|
||||||
paused bool
|
paused bool
|
||||||
@@ -310,6 +311,7 @@ const (
|
|||||||
actBackwardWord
|
actBackwardWord
|
||||||
actCancel
|
actCancel
|
||||||
actChangeBorderLabel
|
actChangeBorderLabel
|
||||||
|
actChangeHeader
|
||||||
actChangePreviewLabel
|
actChangePreviewLabel
|
||||||
actChangePrompt
|
actChangePrompt
|
||||||
actChangeQuery
|
actChangeQuery
|
||||||
@@ -337,6 +339,8 @@ const (
|
|||||||
actToggleUp
|
actToggleUp
|
||||||
actToggleIn
|
actToggleIn
|
||||||
actToggleOut
|
actToggleOut
|
||||||
|
actToggleTrack
|
||||||
|
actTrack
|
||||||
actDown
|
actDown
|
||||||
actUp
|
actUp
|
||||||
actPageUp
|
actPageUp
|
||||||
@@ -350,9 +354,12 @@ const (
|
|||||||
actRefreshPreview
|
actRefreshPreview
|
||||||
actReplaceQuery
|
actReplaceQuery
|
||||||
actToggleSort
|
actToggleSort
|
||||||
|
actShowPreview
|
||||||
|
actHidePreview
|
||||||
actTogglePreview
|
actTogglePreview
|
||||||
actTogglePreviewWrap
|
actTogglePreviewWrap
|
||||||
actTransformBorderLabel
|
actTransformBorderLabel
|
||||||
|
actTransformHeader
|
||||||
actTransformPreviewLabel
|
actTransformPreviewLabel
|
||||||
actTransformPrompt
|
actTransformPrompt
|
||||||
actTransformQuery
|
actTransformQuery
|
||||||
@@ -386,6 +393,7 @@ const (
|
|||||||
actDeselect
|
actDeselect
|
||||||
actUnbind
|
actUnbind
|
||||||
actRebind
|
actRebind
|
||||||
|
actBecome
|
||||||
)
|
)
|
||||||
|
|
||||||
type placeholderFlags struct {
|
type placeholderFlags struct {
|
||||||
@@ -400,6 +408,7 @@ type searchRequest struct {
|
|||||||
sort bool
|
sort bool
|
||||||
sync bool
|
sync bool
|
||||||
command *string
|
command *string
|
||||||
|
changed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type previewRequest struct {
|
type previewRequest struct {
|
||||||
@@ -535,13 +544,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
}
|
}
|
||||||
var previewBox *util.EventBox
|
var previewBox *util.EventBox
|
||||||
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
|
// 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.ListenPort > 0 {
|
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
|
||||||
previewBox = util.NewEventBox()
|
previewBox = util.NewEventBox()
|
||||||
}
|
}
|
||||||
strongAttr := tui.Bold
|
|
||||||
if !opts.Bold {
|
|
||||||
strongAttr = tui.AttrRegular
|
|
||||||
}
|
|
||||||
var renderer tui.Renderer
|
var renderer tui.Renderer
|
||||||
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
|
fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
|
||||||
if fullscreen {
|
if fullscreen {
|
||||||
@@ -601,6 +606,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
multi: opts.Multi,
|
multi: opts.Multi,
|
||||||
sort: opts.Sort > 0,
|
sort: opts.Sort > 0,
|
||||||
toggleSort: opts.ToggleSort,
|
toggleSort: opts.ToggleSort,
|
||||||
|
track: opts.Track,
|
||||||
delimiter: opts.Delimiter,
|
delimiter: opts.Delimiter,
|
||||||
expect: opts.Expect,
|
expect: opts.Expect,
|
||||||
keymap: opts.Keymap,
|
keymap: opts.Keymap,
|
||||||
@@ -620,11 +626,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
previewLabelOpts: opts.PreviewLabel,
|
previewLabelOpts: opts.PreviewLabel,
|
||||||
cleanExit: opts.ClearOnExit,
|
cleanExit: opts.ClearOnExit,
|
||||||
paused: opts.Phony,
|
paused: opts.Phony,
|
||||||
strong: strongAttr,
|
|
||||||
cycle: opts.Cycle,
|
cycle: opts.Cycle,
|
||||||
headerFirst: opts.HeaderFirst,
|
headerFirst: opts.HeaderFirst,
|
||||||
headerLines: opts.HeaderLines,
|
headerLines: opts.HeaderLines,
|
||||||
header: header,
|
header: []string{},
|
||||||
header0: header,
|
header0: header,
|
||||||
ellipsis: opts.Ellipsis,
|
ellipsis: opts.Ellipsis,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
@@ -643,7 +648,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
initialPreviewOpts: opts.Preview,
|
initialPreviewOpts: opts.Preview,
|
||||||
previewOpts: opts.Preview,
|
previewOpts: opts.Preview,
|
||||||
previewer: previewer{0, []string{}, 0, len(opts.Preview.command) > 0, false, true, disabledState, "", []bool{}},
|
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
|
||||||
previewed: previewed{0, 0, 0, false},
|
previewed: previewed{0, 0, 0, false},
|
||||||
previewBox: previewBox,
|
previewBox: previewBox,
|
||||||
eventBox: eventBox,
|
eventBox: eventBox,
|
||||||
@@ -691,13 +696,25 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
|
|
||||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||||
|
|
||||||
if err := startHttpServer(t.listenPort, t.serverChan); err != nil {
|
if t.listenPort != nil {
|
||||||
errorExit(err.Error())
|
err, port := startHttpServer(*t.listenPort, t.serverChan)
|
||||||
|
if err != nil {
|
||||||
|
errorExit(err.Error())
|
||||||
|
}
|
||||||
|
t.listenPort = &port
|
||||||
}
|
}
|
||||||
|
|
||||||
return &t
|
return &t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) environ() []string {
|
||||||
|
env := os.Environ()
|
||||||
|
if t.listenPort != nil {
|
||||||
|
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
func borderLines(shape tui.BorderShape) int {
|
func borderLines(shape tui.BorderShape) int {
|
||||||
switch shape {
|
switch shape {
|
||||||
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
||||||
@@ -737,7 +754,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool)
|
|||||||
|
|
||||||
// Simpler printer for strings without ANSI colors or tab characters
|
// Simpler printer for strings without ANSI colors or tab characters
|
||||||
if colors == nil && strings.IndexRune(str, '\t') < 0 {
|
if colors == nil && strings.IndexRune(str, '\t') < 0 {
|
||||||
length := runewidth.StringWidth(str)
|
length := util.StringWidth(str)
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
@@ -871,10 +888,21 @@ func reverseStringArray(input []string) []string {
|
|||||||
return reversed
|
return reversed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) changeHeader(header string) bool {
|
||||||
|
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
|
||||||
|
switch t.layout {
|
||||||
|
case layoutDefault, layoutReverseList:
|
||||||
|
lines = reverseStringArray(lines)
|
||||||
|
}
|
||||||
|
needFullRedraw := len(t.header0) != len(lines)
|
||||||
|
t.header0 = lines
|
||||||
|
return needFullRedraw
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateHeader updates the header
|
// UpdateHeader updates the header
|
||||||
func (t *Terminal) UpdateHeader(header []string) {
|
func (t *Terminal) UpdateHeader(header []string) {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
t.header = append(append([]string{}, t.header0...), header...)
|
t.header = header
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
t.reqBox.Set(reqHeader, nil)
|
t.reqBox.Set(reqHeader, nil)
|
||||||
}
|
}
|
||||||
@@ -895,16 +923,54 @@ func (t *Terminal) UpdateProgress(progress float32) {
|
|||||||
// UpdateList updates Merger to display the list
|
// UpdateList updates Merger to display the list
|
||||||
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
|
var prevIndex int32 = -1
|
||||||
|
if !reset && t.track != trackDisabled {
|
||||||
|
if t.merger.Length() > 0 {
|
||||||
|
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||||
|
} else if merger.Length() > 0 {
|
||||||
|
prevIndex = merger.First().item.Index()
|
||||||
|
}
|
||||||
|
}
|
||||||
t.progress = 100
|
t.progress = 100
|
||||||
t.merger = merger
|
t.merger = merger
|
||||||
if reset {
|
if reset {
|
||||||
t.selected = make(map[int32]selectedItem)
|
t.selected = make(map[int32]selectedItem)
|
||||||
t.version++
|
t.version++
|
||||||
}
|
}
|
||||||
if t.hasLoadActions && t.triggerLoad {
|
if t.triggerLoad {
|
||||||
t.triggerLoad = false
|
t.triggerLoad = false
|
||||||
t.eventChan <- tui.Load.AsEvent()
|
t.eventChan <- tui.Load.AsEvent()
|
||||||
}
|
}
|
||||||
|
if prevIndex >= 0 {
|
||||||
|
pos := t.cy - t.offset
|
||||||
|
count := t.merger.Length()
|
||||||
|
i := t.merger.FindIndex(prevIndex)
|
||||||
|
if i >= 0 {
|
||||||
|
t.cy = i
|
||||||
|
t.offset = t.cy - pos
|
||||||
|
} else if t.track == trackCurrent {
|
||||||
|
t.track = trackDisabled
|
||||||
|
t.cy = pos
|
||||||
|
t.offset = 0
|
||||||
|
} else if t.cy > count {
|
||||||
|
// Try to keep the vertical position when the list shrinks
|
||||||
|
t.cy = count - util.Min(count, t.maxItems()) + pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !t.reading {
|
||||||
|
switch t.merger.Length() {
|
||||||
|
case 0:
|
||||||
|
zero := tui.Zero.AsEvent()
|
||||||
|
if _, prs := t.keymap[zero]; prs {
|
||||||
|
t.eventChan <- zero
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
one := tui.One.AsEvent()
|
||||||
|
if _, prs := t.keymap[one]; prs {
|
||||||
|
t.eventChan <- one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
t.reqBox.Set(reqInfo, nil)
|
t.reqBox.Set(reqInfo, nil)
|
||||||
t.reqBox.Set(reqList, nil)
|
t.reqBox.Set(reqList, nil)
|
||||||
@@ -1032,7 +1098,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
|||||||
if t.noInfoLine() {
|
if t.noInfoLine() {
|
||||||
minAreaHeight -= 1
|
minAreaHeight -= 1
|
||||||
}
|
}
|
||||||
if t.mayNeedPreviewWindow() {
|
if t.needPreviewWindow() {
|
||||||
minPreviewHeight := 1 + borderLines(t.previewOpts.border)
|
minPreviewHeight := 1 + borderLines(t.previewOpts.border)
|
||||||
minPreviewWidth := 5
|
minPreviewWidth := 5
|
||||||
switch t.previewOpts.position {
|
switch t.previewOpts.position {
|
||||||
@@ -1115,7 +1181,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
|||||||
|
|
||||||
// Set up preview window
|
// Set up preview window
|
||||||
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
|
||||||
if t.mayNeedPreviewWindow() {
|
if forcePreview || t.needPreviewWindow() {
|
||||||
var resizePreviewWindows func(previewOpts *previewOpts)
|
var resizePreviewWindows func(previewOpts *previewOpts)
|
||||||
resizePreviewWindows = func(previewOpts *previewOpts) {
|
resizePreviewWindows = func(previewOpts *previewOpts) {
|
||||||
t.activePreviewOpts = previewOpts
|
t.activePreviewOpts = previewOpts
|
||||||
@@ -1244,6 +1310,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
resizePreviewWindows(&t.previewOpts)
|
resizePreviewWindows(&t.previewOpts)
|
||||||
|
} else {
|
||||||
|
t.activePreviewOpts = &t.previewOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without preview window
|
// Without preview window
|
||||||
@@ -1305,7 +1373,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
|
|||||||
case layoutDefault:
|
case layoutDefault:
|
||||||
y = h - y - 1
|
y = h - y - 1
|
||||||
case layoutReverseList:
|
case layoutReverseList:
|
||||||
n := 2 + len(t.header)
|
n := 2 + len(t.header0) + len(t.header)
|
||||||
if t.noInfoLine() {
|
if t.noInfoLine() {
|
||||||
n--
|
n--
|
||||||
}
|
}
|
||||||
@@ -1333,8 +1401,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
|||||||
|
|
||||||
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
|
_, overflow := t.trimLeft(t.input[:t.cx], maxWidth)
|
||||||
minOffset := int(overflow)
|
minOffset := int(overflow)
|
||||||
maxOffset := util.Min(util.Min(len(t.input), minOffset+maxWidth), t.cx)
|
maxOffset := minOffset + (maxWidth-util.Max(0, maxWidth-t.cx))/2
|
||||||
|
|
||||||
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
|
t.xoffset = util.Constrain(t.xoffset, minOffset, maxOffset)
|
||||||
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
|
before, _ := t.trimLeft(t.input[t.xoffset:t.cx], maxWidth)
|
||||||
beforeLen := t.displayWidth(before)
|
beforeLen := t.displayWidth(before)
|
||||||
@@ -1399,7 +1466,7 @@ func (t *Terminal) printInfo() {
|
|||||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||||
str := t.infoSep
|
str := t.infoSep
|
||||||
maxWidth := t.window.Width() - pos
|
maxWidth := t.window.Width() - pos
|
||||||
width := runewidth.StringWidth(str)
|
width := util.StringWidth(str)
|
||||||
if width > maxWidth {
|
if width > maxWidth {
|
||||||
trimmed, _ := t.trimRight([]rune(str), maxWidth)
|
trimmed, _ := t.trimRight([]rune(str), maxWidth)
|
||||||
str = string(trimmed)
|
str = string(trimmed)
|
||||||
@@ -1426,6 +1493,9 @@ func (t *Terminal) printInfo() {
|
|||||||
output += " -S"
|
output += " -S"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if t.track != trackDisabled {
|
||||||
|
output += " +T"
|
||||||
|
}
|
||||||
if t.multi > 0 {
|
if t.multi > 0 {
|
||||||
if t.multi == maxMulti {
|
if t.multi == maxMulti {
|
||||||
output += fmt.Sprintf(" (%d)", len(t.selected))
|
output += fmt.Sprintf(" (%d)", len(t.selected))
|
||||||
@@ -1451,7 +1521,7 @@ func (t *Terminal) printInfo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printHeader() {
|
func (t *Terminal) printHeader() {
|
||||||
if len(t.header) == 0 {
|
if len(t.header0)+len(t.header) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
max := t.window.Height()
|
max := t.window.Height()
|
||||||
@@ -1462,7 +1532,7 @@ func (t *Terminal) printHeader() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var state *ansiState
|
var state *ansiState
|
||||||
for idx, lineStr := range t.header {
|
for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
|
||||||
line := idx
|
line := idx
|
||||||
if !t.headerFirst {
|
if !t.headerFirst {
|
||||||
line++
|
line++
|
||||||
@@ -1496,7 +1566,7 @@ func (t *Terminal) printList() {
|
|||||||
if t.layout == layoutDefault {
|
if t.layout == layoutDefault {
|
||||||
i = maxy - 1 - j
|
i = maxy - 1 - j
|
||||||
}
|
}
|
||||||
line := i + 2 + len(t.header)
|
line := i + 2 + len(t.header0) + len(t.header)
|
||||||
if t.noInfoLine() {
|
if t.noInfoLine() {
|
||||||
line--
|
line--
|
||||||
}
|
}
|
||||||
@@ -1802,7 +1872,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
|||||||
if ansi != nil {
|
if ansi != nil {
|
||||||
ansi.lbg = -1
|
ansi.lbg = -1
|
||||||
}
|
}
|
||||||
line = strings.TrimSuffix(line, "\n")
|
line = strings.TrimRight(line, "\r\n")
|
||||||
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||||
t.previewed.filled = true
|
t.previewed.filled = true
|
||||||
t.previewer.scrollable = true
|
t.previewer.scrollable = true
|
||||||
@@ -1824,12 +1894,14 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
|
|||||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||||
}
|
}
|
||||||
str, width := t.processTabs(trimmed, prefixWidth)
|
str, width := t.processTabs(trimmed, prefixWidth)
|
||||||
prefixWidth += width
|
if width > prefixWidth {
|
||||||
if t.theme.Colored && ansi != nil && ansi.colored() {
|
prefixWidth = width
|
||||||
lbg = ansi.lbg
|
if t.theme.Colored && ansi != nil && ansi.colored() {
|
||||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
|
lbg = ansi.lbg
|
||||||
} else {
|
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
|
||||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
} else {
|
||||||
|
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return !isTrimmed &&
|
return !isTrimmed &&
|
||||||
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||||
@@ -1932,7 +2004,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
|||||||
w = t.tabstop - l%t.tabstop
|
w = t.tabstop - l%t.tabstop
|
||||||
strbuf.WriteString(strings.Repeat(" ", w))
|
strbuf.WriteString(strings.Repeat(" ", w))
|
||||||
} else {
|
} else {
|
||||||
w = runewidth.StringWidth(str)
|
w = util.StringWidth(str)
|
||||||
strbuf.WriteString(str)
|
strbuf.WriteString(str)
|
||||||
}
|
}
|
||||||
l += w
|
l += w
|
||||||
@@ -2232,16 +2304,17 @@ func (t *Terminal) redraw() {
|
|||||||
t.printAll()
|
t.printAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
|
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
|
||||||
line := ""
|
line := ""
|
||||||
valid, list := t.buildPlusList(template, forcePlus, false)
|
valid, list := t.buildPlusList(template, forcePlus)
|
||||||
// captureFirstLine is used for transform-{prompt,query} and we don't want to
|
// 'capture' is used for transform-* and we don't want to
|
||||||
// return an empty string in those cases
|
// return an empty string in those cases
|
||||||
if !valid && !captureFirstLine {
|
if !valid && !capture {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||||
cmd := util.ExecCommand(command, false)
|
cmd := util.ExecCommand(command, false)
|
||||||
|
cmd.Env = t.environ()
|
||||||
t.executing.Set(true)
|
t.executing.Set(true)
|
||||||
if !background {
|
if !background {
|
||||||
cmd.Stdin = tui.TtyIn()
|
cmd.Stdin = tui.TtyIn()
|
||||||
@@ -2253,12 +2326,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
|||||||
t.redraw()
|
t.redraw()
|
||||||
t.refresh()
|
t.refresh()
|
||||||
} else {
|
} else {
|
||||||
if captureFirstLine {
|
if capture {
|
||||||
out, _ := cmd.StdoutPipe()
|
out, _ := cmd.StdoutPipe()
|
||||||
reader := bufio.NewReader(out)
|
reader := bufio.NewReader(out)
|
||||||
cmd.Start()
|
cmd.Start()
|
||||||
line, _ = reader.ReadString('\n')
|
if firstLineOnly {
|
||||||
line = strings.TrimRight(line, "\r\n")
|
line, _ = reader.ReadString('\n')
|
||||||
|
line = strings.TrimRight(line, "\r\n")
|
||||||
|
} else {
|
||||||
|
bytes, _ := io.ReadAll(reader)
|
||||||
|
line = string(bytes)
|
||||||
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
} else {
|
} else {
|
||||||
cmd.Run()
|
cmd.Run()
|
||||||
@@ -2273,13 +2351,13 @@ func (t *Terminal) hasPreviewer() bool {
|
|||||||
return t.previewBox != nil
|
return t.previewBox != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) mayNeedPreviewWindow() bool {
|
func (t *Terminal) needPreviewWindow() bool {
|
||||||
return t.hasPreviewer() && t.previewer.enabled && t.previewOpts.Visible()
|
return t.hasPreviewer() && len(t.previewOpts.command) > 0 && t.previewOpts.Visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
|
// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer)
|
||||||
func (t *Terminal) isPreviewEnabled() bool {
|
func (t *Terminal) canPreview() bool {
|
||||||
return t.hasPreviewer() && t.previewer.enabled && (!t.previewOpts.Visible() || t.pwindow != nil)
|
return t.hasPreviewer() && (!t.previewOpts.Visible() && !t.previewOpts.hidden || t.hasPreviewWindow())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) hasPreviewWindow() bool {
|
func (t *Terminal) hasPreviewWindow() bool {
|
||||||
@@ -2294,10 +2372,10 @@ func (t *Terminal) currentItem() *Item {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) buildPlusList(template string, forcePlus bool, forceEvaluation bool) (bool, []*Item) {
|
func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) {
|
||||||
current := t.currentItem()
|
current := t.currentItem()
|
||||||
slot, plus, query := hasPreviewFlags(template)
|
slot, plus, query := hasPreviewFlags(template)
|
||||||
if !forceEvaluation && !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) {
|
||||||
return current != nil, []*Item{current, current}
|
return current != nil, []*Item{current, current}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2387,7 +2465,7 @@ func (t *Terminal) Loop() {
|
|||||||
pad := fitpad.pad
|
pad := fitpad.pad
|
||||||
t.tui.Resize(func(termHeight int) int {
|
t.tui.Resize(func(termHeight int) int {
|
||||||
contentHeight := fit + t.extraLines()
|
contentHeight := fit + t.extraLines()
|
||||||
if t.mayNeedPreviewWindow() {
|
if t.needPreviewWindow() {
|
||||||
if t.previewOpts.aboveOrBelow() {
|
if t.previewOpts.aboveOrBelow() {
|
||||||
if t.previewOpts.size.percent {
|
if t.previewOpts.size.percent {
|
||||||
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size))
|
newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size))
|
||||||
@@ -2488,17 +2566,17 @@ func (t *Terminal) Loop() {
|
|||||||
_, query := t.Input()
|
_, query := t.Input()
|
||||||
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
||||||
cmd := util.ExecCommand(command, true)
|
cmd := util.ExecCommand(command, true)
|
||||||
|
env := t.environ()
|
||||||
if pwindow != nil {
|
if pwindow != nil {
|
||||||
height := pwindow.Height()
|
height := pwindow.Height()
|
||||||
env := os.Environ()
|
|
||||||
lines := fmt.Sprintf("LINES=%d", height)
|
lines := fmt.Sprintf("LINES=%d", height)
|
||||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||||
env = append(env, lines)
|
env = append(env, lines)
|
||||||
env = append(env, "FZF_PREVIEW_"+lines)
|
env = append(env, "FZF_PREVIEW_"+lines)
|
||||||
env = append(env, columns)
|
env = append(env, columns)
|
||||||
env = append(env, "FZF_PREVIEW_"+columns)
|
env = append(env, "FZF_PREVIEW_"+columns)
|
||||||
cmd.Env = env
|
|
||||||
}
|
}
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
out, _ := cmd.StdoutPipe()
|
out, _ := cmd.StdoutPipe()
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stderr = cmd.Stdout
|
||||||
@@ -2621,8 +2699,8 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshPreview := func(command string) {
|
refreshPreview := func(command string) {
|
||||||
if len(command) > 0 && t.isPreviewEnabled() {
|
if len(command) > 0 && t.canPreview() {
|
||||||
_, list := t.buildPlusList(command, false, false)
|
_, list := t.buildPlusList(command, false)
|
||||||
t.cancelPreview()
|
t.cancelPreview()
|
||||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
|
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
|
||||||
}
|
}
|
||||||
@@ -2669,6 +2747,10 @@ func (t *Terminal) Loop() {
|
|||||||
currentIndex = currentItem.Index()
|
currentIndex = currentItem.Index()
|
||||||
}
|
}
|
||||||
focusChanged := focusedIndex != currentIndex
|
focusChanged := focusedIndex != currentIndex
|
||||||
|
if focusChanged && t.track == trackCurrent {
|
||||||
|
t.track = trackDisabled
|
||||||
|
t.printInfo()
|
||||||
|
}
|
||||||
if onFocus != nil && focusChanged {
|
if onFocus != nil && focusChanged {
|
||||||
t.serverChan <- onFocus
|
t.serverChan <- onFocus
|
||||||
}
|
}
|
||||||
@@ -2696,7 +2778,7 @@ func (t *Terminal) Loop() {
|
|||||||
case reqFullRedraw:
|
case reqFullRedraw:
|
||||||
wasHidden := t.pwindow == nil
|
wasHidden := t.pwindow == nil
|
||||||
t.redraw()
|
t.redraw()
|
||||||
if wasHidden && t.pwindow != nil {
|
if wasHidden && t.hasPreviewWindow() {
|
||||||
refreshPreview(t.previewOpts.command)
|
refreshPreview(t.previewOpts.command)
|
||||||
}
|
}
|
||||||
case reqClose:
|
case reqClose:
|
||||||
@@ -2781,7 +2863,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case event = <-t.eventChan:
|
case event = <-t.eventChan:
|
||||||
needBarrier = event != tui.Load.AsEvent()
|
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||||
case actions = <-t.serverChan:
|
case actions = <-t.serverChan:
|
||||||
event = tui.Invalid.AsEvent()
|
event = tui.Invalid.AsEvent()
|
||||||
needBarrier = false
|
needBarrier = false
|
||||||
@@ -2857,23 +2939,46 @@ func (t *Terminal) Loop() {
|
|||||||
doAction = func(a *action) bool {
|
doAction = func(a *action) bool {
|
||||||
switch a.t {
|
switch a.t {
|
||||||
case actIgnore:
|
case actIgnore:
|
||||||
|
case actBecome:
|
||||||
|
valid, list := t.buildPlusList(a.a, false)
|
||||||
|
if valid {
|
||||||
|
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = "sh"
|
||||||
|
}
|
||||||
|
shellPath, err := exec.LookPath(shell)
|
||||||
|
if err == nil {
|
||||||
|
t.tui.Close()
|
||||||
|
if t.history != nil {
|
||||||
|
t.history.append(string(t.input))
|
||||||
|
}
|
||||||
|
util.SetStdin(tui.TtyIn())
|
||||||
|
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
|
||||||
|
}
|
||||||
|
}
|
||||||
case actExecute, actExecuteSilent:
|
case actExecute, actExecuteSilent:
|
||||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
|
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
||||||
case actExecuteMulti:
|
case actExecuteMulti:
|
||||||
t.executeCommand(a.a, true, false, false)
|
t.executeCommand(a.a, true, false, false, false)
|
||||||
case actInvalid:
|
case actInvalid:
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
return false
|
return false
|
||||||
case actTogglePreview:
|
case actTogglePreview, actShowPreview, actHidePreview:
|
||||||
if t.hasPreviewer() {
|
var act bool
|
||||||
if t.activePreviewOpts != nil {
|
switch a.t {
|
||||||
t.activePreviewOpts.Toggle()
|
case actShowPreview:
|
||||||
} else if !t.previewOpts.Visible() {
|
act = !t.hasPreviewWindow() && len(t.previewOpts.command) > 0
|
||||||
t.previewer.enabled = !t.previewer.enabled
|
case actHidePreview:
|
||||||
}
|
act = t.hasPreviewWindow()
|
||||||
|
case actTogglePreview:
|
||||||
|
act = t.hasPreviewWindow() || len(t.previewOpts.command) > 0
|
||||||
|
}
|
||||||
|
if act {
|
||||||
|
t.activePreviewOpts.Toggle()
|
||||||
updatePreviewWindow(false)
|
updatePreviewWindow(false)
|
||||||
if t.isPreviewEnabled() {
|
if t.canPreview() {
|
||||||
valid, list := t.buildPlusList(t.previewOpts.command, false, false)
|
valid, list := t.buildPlusList(t.previewOpts.command, false)
|
||||||
if valid {
|
if valid {
|
||||||
t.cancelPreview()
|
t.cancelPreview()
|
||||||
t.previewBox.Set(reqPreviewEnqueue,
|
t.previewBox.Set(reqPreviewEnqueue,
|
||||||
@@ -2889,11 +2994,11 @@ func (t *Terminal) Loop() {
|
|||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
}
|
}
|
||||||
case actTransformPrompt:
|
case actTransformPrompt:
|
||||||
prompt := t.executeCommand(a.a, false, true, true)
|
prompt := t.executeCommand(a.a, false, true, true, true)
|
||||||
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
t.prompt, t.promptLen = t.parsePrompt(prompt)
|
||||||
req(reqPrompt)
|
req(reqPrompt)
|
||||||
case actTransformQuery:
|
case actTransformQuery:
|
||||||
query := t.executeCommand(a.a, false, true, true)
|
query := t.executeCommand(a.a, false, true, true, true)
|
||||||
t.input = []rune(query)
|
t.input = []rune(query)
|
||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
case actToggleSort:
|
case actToggleSort:
|
||||||
@@ -2942,6 +3047,19 @@ func (t *Terminal) Loop() {
|
|||||||
case actChangeQuery:
|
case actChangeQuery:
|
||||||
t.input = []rune(a.a)
|
t.input = []rune(a.a)
|
||||||
t.cx = len(t.input)
|
t.cx = len(t.input)
|
||||||
|
case actTransformHeader:
|
||||||
|
header := t.executeCommand(a.a, false, true, true, false)
|
||||||
|
if t.changeHeader(header) {
|
||||||
|
req(reqFullRedraw)
|
||||||
|
} else {
|
||||||
|
req(reqHeader)
|
||||||
|
}
|
||||||
|
case actChangeHeader:
|
||||||
|
if t.changeHeader(a.a) {
|
||||||
|
req(reqFullRedraw)
|
||||||
|
} else {
|
||||||
|
req(reqHeader)
|
||||||
|
}
|
||||||
case actChangeBorderLabel:
|
case actChangeBorderLabel:
|
||||||
if t.border != nil {
|
if t.border != nil {
|
||||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
||||||
@@ -2954,13 +3072,13 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
case actTransformBorderLabel:
|
case actTransformBorderLabel:
|
||||||
if t.border != nil {
|
if t.border != nil {
|
||||||
label := t.executeCommand(a.a, false, true, true)
|
label := t.executeCommand(a.a, false, true, true, true)
|
||||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||||
req(reqRedrawBorderLabel)
|
req(reqRedrawBorderLabel)
|
||||||
}
|
}
|
||||||
case actTransformPreviewLabel:
|
case actTransformPreviewLabel:
|
||||||
if t.pborder != nil {
|
if t.pborder != nil {
|
||||||
label := t.executeCommand(a.a, false, true, true)
|
label := t.executeCommand(a.a, false, true, true, true)
|
||||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||||
req(reqRedrawPreviewLabel)
|
req(reqRedrawPreviewLabel)
|
||||||
}
|
}
|
||||||
@@ -2968,7 +3086,6 @@ func (t *Terminal) Loop() {
|
|||||||
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
t.prompt, t.promptLen = t.parsePrompt(a.a)
|
||||||
req(reqPrompt)
|
req(reqPrompt)
|
||||||
case actPreview:
|
case actPreview:
|
||||||
t.previewer.enabled = true
|
|
||||||
updatePreviewWindow(true)
|
updatePreviewWindow(true)
|
||||||
refreshPreview(a.a)
|
refreshPreview(a.a)
|
||||||
case actRefreshPreview:
|
case actRefreshPreview:
|
||||||
@@ -3211,6 +3328,19 @@ func (t *Terminal) Loop() {
|
|||||||
t.paused = !t.paused
|
t.paused = !t.paused
|
||||||
changed = !t.paused
|
changed = !t.paused
|
||||||
req(reqPrompt)
|
req(reqPrompt)
|
||||||
|
case actToggleTrack:
|
||||||
|
switch t.track {
|
||||||
|
case trackEnabled:
|
||||||
|
t.track = trackDisabled
|
||||||
|
case trackDisabled:
|
||||||
|
t.track = trackEnabled
|
||||||
|
}
|
||||||
|
req(reqInfo)
|
||||||
|
case actTrack:
|
||||||
|
if t.track == trackDisabled {
|
||||||
|
t.track = trackCurrent
|
||||||
|
}
|
||||||
|
req(reqInfo)
|
||||||
case actEnableSearch:
|
case actEnableSearch:
|
||||||
t.paused = false
|
t.paused = false
|
||||||
changed = true
|
changed = true
|
||||||
@@ -3262,7 +3392,7 @@ func (t *Terminal) Loop() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevew scrollbar dragging
|
// Preview scrollbar dragging
|
||||||
headerLines := t.previewOpts.headerLines
|
headerLines := t.previewOpts.headerLines
|
||||||
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
|
pbarDragging = me.Down && (pbarDragging || clicked && t.hasPreviewWindow() && my >= t.pwindow.Top()+headerLines && my < t.pwindow.Top()+t.pwindow.Height() && mx == t.pwindow.Left()+t.pwindow.Width())
|
||||||
if pbarDragging {
|
if pbarDragging {
|
||||||
@@ -3288,7 +3418,7 @@ func (t *Terminal) Loop() {
|
|||||||
// Translate coordinates
|
// Translate coordinates
|
||||||
mx -= t.window.Left()
|
mx -= t.window.Left()
|
||||||
my -= t.window.Top()
|
my -= t.window.Top()
|
||||||
min := 2 + len(t.header)
|
min := 2 + len(t.header0) + len(t.header)
|
||||||
if t.noInfoLine() {
|
if t.noInfoLine() {
|
||||||
min--
|
min--
|
||||||
}
|
}
|
||||||
@@ -3353,7 +3483,7 @@ func (t *Terminal) Loop() {
|
|||||||
case actReload, actReloadSync:
|
case actReload, actReloadSync:
|
||||||
t.failed = nil
|
t.failed = nil
|
||||||
|
|
||||||
valid, list := t.buildPlusList(a.a, false, false)
|
valid, list := t.buildPlusList(a.a, false)
|
||||||
if !valid {
|
if !valid {
|
||||||
// We run the command even when there's no match
|
// We run the command even when there's no match
|
||||||
// 1. If the template doesn't have any slots
|
// 1. If the template doesn't have any slots
|
||||||
@@ -3381,9 +3511,8 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
case actChangePreview:
|
case actChangePreview:
|
||||||
if t.previewOpts.command != a.a {
|
if t.previewOpts.command != a.a {
|
||||||
t.previewer.enabled = len(a.a) > 0
|
|
||||||
updatePreviewWindow(false)
|
|
||||||
t.previewOpts.command = a.a
|
t.previewOpts.command = a.a
|
||||||
|
updatePreviewWindow(false)
|
||||||
refreshPreview(t.previewOpts.command)
|
refreshPreview(t.previewOpts.command)
|
||||||
}
|
}
|
||||||
case actChangePreviewWindow:
|
case actChangePreviewWindow:
|
||||||
@@ -3403,7 +3532,7 @@ func (t *Terminal) Loop() {
|
|||||||
if !currentPreviewOpts.sameLayout(t.previewOpts) {
|
if !currentPreviewOpts.sameLayout(t.previewOpts) {
|
||||||
wasHidden := t.pwindow == nil
|
wasHidden := t.pwindow == nil
|
||||||
updatePreviewWindow(false)
|
updatePreviewWindow(false)
|
||||||
if wasHidden && t.pwindow != nil {
|
if wasHidden && t.hasPreviewWindow() {
|
||||||
refreshPreview(t.previewOpts.command)
|
refreshPreview(t.previewOpts.command)
|
||||||
} else {
|
} else {
|
||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
@@ -3480,12 +3609,10 @@ func (t *Terminal) Loop() {
|
|||||||
req(reqList)
|
req(reqList)
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryChanged {
|
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
|
||||||
if t.isPreviewEnabled() {
|
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
||||||
_, _, q := hasPreviewFlags(t.previewOpts.command)
|
if q {
|
||||||
if q {
|
t.version++
|
||||||
t.version++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3496,7 +3623,7 @@ func (t *Terminal) Loop() {
|
|||||||
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
t.mutex.Unlock() // Must be unlocked before touching reqBox
|
||||||
|
|
||||||
if changed || newCommand != nil {
|
if changed || newCommand != nil {
|
||||||
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand})
|
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
|
||||||
}
|
}
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
t.reqBox.Set(event, nil)
|
t.reqBox.Set(event, nil)
|
||||||
@@ -3560,7 +3687,7 @@ func (t *Terminal) vset(o int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) maxItems() int {
|
func (t *Terminal) maxItems() int {
|
||||||
max := t.window.Height() - 2 - len(t.header)
|
max := t.window.Height() - 2 - len(t.header0) - len(t.header)
|
||||||
if t.noInfoLine() {
|
if t.noInfoLine() {
|
||||||
max++
|
max++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,20 +32,26 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]
|
|||||||
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||||
|
|
||||||
func (r *LightRenderer) stderr(str string) {
|
func (r *LightRenderer) stderr(str string) {
|
||||||
r.stderrInternal(str, true)
|
r.stderrInternal(str, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Need better handling of non-displayable characters
|
const CR string = "\x1b[2m␍"
|
||||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
const LF string = "\x1b[2m␊"
|
||||||
|
|
||||||
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
bytes := []byte(str)
|
bytes := []byte(str)
|
||||||
runes := []rune{}
|
runes := []rune{}
|
||||||
for len(bytes) > 0 {
|
for len(bytes) > 0 {
|
||||||
r, sz := utf8.DecodeRune(bytes)
|
r, sz := utf8.DecodeRune(bytes)
|
||||||
nlcr := r == '\n' || r == '\r'
|
nlcr := r == '\n' || r == '\r'
|
||||||
if r >= 32 || r == '\x1b' || nlcr {
|
if r >= 32 || r == '\x1b' || nlcr {
|
||||||
if r == utf8.RuneError || nlcr && !allowNLCR {
|
if nlcr && !allowNLCR {
|
||||||
runes = append(runes, ' ')
|
if r == '\r' {
|
||||||
} else {
|
runes = append(runes, []rune(CR+resetCode)...)
|
||||||
|
} else {
|
||||||
|
runes = append(runes, []rune(LF+resetCode)...)
|
||||||
|
}
|
||||||
|
} else if r != utf8.RuneError {
|
||||||
runes = append(runes, r)
|
runes = append(runes, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +60,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
|||||||
r.queued.WriteString(string(runes))
|
r.queued.WriteString(string(runes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) csi(code string) {
|
func (r *LightRenderer) csi(code string) string {
|
||||||
r.stderr("\x1b[" + code)
|
fullcode := "\x1b[" + code
|
||||||
|
r.stderr(fullcode)
|
||||||
|
return fullcode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) flush() {
|
func (r *LightRenderer) flush() {
|
||||||
@@ -825,12 +833,12 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
|||||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) csi(code string) {
|
func (w *LightWindow) csi(code string) string {
|
||||||
w.renderer.csi(code)
|
return w.renderer.csi(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
w.renderer.stderrInternal(str, allowNLCR)
|
w.renderer.stderrInternal(str, allowNLCR, resetCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Top() int {
|
func (w *LightWindow) Top() int {
|
||||||
@@ -936,10 +944,10 @@ func colorCodes(fg Color, bg Color) []string {
|
|||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
|
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
|
||||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||||
w.csi(";" + strings.Join(codes, ";") + "m")
|
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||||
return len(codes) > 0
|
return len(codes) > 0, code
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Print(text string) {
|
func (w *LightWindow) Print(text string) {
|
||||||
@@ -951,16 +959,17 @@ func cleanse(str string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||||
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||||
w.stderrInternal(cleanse(text), false)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
w.csi("m")
|
w.csi("m")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||||
if w.csiColor(fg, bg, attr) {
|
hasColors, code := w.csiColor(fg, bg, attr)
|
||||||
|
if hasColors {
|
||||||
defer w.csi("m")
|
defer w.csi("m")
|
||||||
}
|
}
|
||||||
w.stderrInternal(cleanse(text), false)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
type wrappedLine struct {
|
type wrappedLine struct {
|
||||||
@@ -980,6 +989,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
if len(rs) == 1 && rs[0] == '\t' {
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
w = tabstop - (prefixLength+width)%tabstop
|
w = tabstop - (prefixLength+width)%tabstop
|
||||||
str = repeat(' ', w)
|
str = repeat(' ', w)
|
||||||
|
} else if rs[0] == '\r' {
|
||||||
|
w++
|
||||||
} else {
|
} else {
|
||||||
w = runewidth.StringWidth(str)
|
w = runewidth.StringWidth(str)
|
||||||
}
|
}
|
||||||
@@ -998,12 +1009,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
for i, line := range allLines {
|
||||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||||
for j, wl := range lines {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false)
|
w.stderrInternal(wl.text, false, resetCode)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.displayWidth
|
||||||
|
|
||||||
// Wrap line
|
// Wrap line
|
||||||
@@ -1013,7 +1024,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
|||||||
}
|
}
|
||||||
w.MoveAndClear(w.posy, w.posx)
|
w.MoveAndClear(w.posy, w.posx)
|
||||||
w.Move(w.posy+1, 0)
|
w.Move(w.posy+1, 0)
|
||||||
onMove()
|
w.renderer.stderr(resetCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1022,22 +1033,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
|||||||
return FillSuspend
|
return FillSuspend
|
||||||
}
|
}
|
||||||
w.Move(w.posy+1, 0)
|
w.Move(w.posy+1, 0)
|
||||||
onMove()
|
w.renderer.stderr(resetCode)
|
||||||
return FillNextLine
|
return FillNextLine
|
||||||
}
|
}
|
||||||
return FillContinue
|
return FillContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) setBg() {
|
func (w *LightWindow) setBg() string {
|
||||||
if w.bg != colDefault {
|
if w.bg != colDefault {
|
||||||
w.csiColor(colDefault, w.bg, AttrRegular)
|
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
|
||||||
|
return code
|
||||||
}
|
}
|
||||||
|
// Should clear dim attribute after ␍ in the preview window
|
||||||
|
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
|
||||||
|
return "\x1b[m"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Fill(text string) FillReturn {
|
func (w *LightWindow) Fill(text string) FillReturn {
|
||||||
w.Move(w.posy, w.posx)
|
w.Move(w.posy, w.posx)
|
||||||
w.setBg()
|
code := w.setBg()
|
||||||
return w.fill(text, w.setBg)
|
return w.fill(text, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||||
@@ -1048,11 +1063,11 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
|||||||
if bg == colDefault {
|
if bg == colDefault {
|
||||||
bg = w.bg
|
bg = w.bg
|
||||||
}
|
}
|
||||||
if w.csiColor(fg, bg, attr) {
|
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||||
defer w.csi("m")
|
defer w.csi("m")
|
||||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
return w.fill(text, resetCode)
|
||||||
}
|
}
|
||||||
return w.fill(text, w.setBg)
|
return w.fill(text, w.setBg())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) FinishFill() {
|
func (w *LightWindow) FinishFill() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/gdamore/tcell/v2/encoding"
|
"github.com/gdamore/tcell/v2/encoding"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/rivo/uniseg"
|
"github.com/rivo/uniseg"
|
||||||
@@ -572,26 +573,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
|||||||
|
|
||||||
gr := uniseg.NewGraphemes(text)
|
gr := uniseg.NewGraphemes(text)
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
|
st := style
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
|
|
||||||
if len(rs) == 1 {
|
if len(rs) == 1 {
|
||||||
r := rs[0]
|
r := rs[0]
|
||||||
if r < rune(' ') { // ignore control characters
|
if r == '\r' {
|
||||||
continue
|
st = style.Dim(true)
|
||||||
|
rs[0] = '␍'
|
||||||
} else if r == '\n' {
|
} else if r == '\n' {
|
||||||
w.lastY++
|
st = style.Dim(true)
|
||||||
lx = 0
|
rs[0] = '␊'
|
||||||
continue
|
} else if r < rune(' ') { // ignore control characters
|
||||||
} else if r == '\u000D' { // skip carriage return
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var xPos = w.left + w.lastX + lx
|
var xPos = w.left + w.lastX + lx
|
||||||
var yPos = w.top + w.lastY
|
var yPos = w.top + w.lastY
|
||||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
}
|
}
|
||||||
lx += runewidth.StringWidth(string(rs))
|
lx += util.StringWidth(string(rs))
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
}
|
}
|
||||||
@@ -620,13 +622,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||||
|
|
||||||
gr := uniseg.NewGraphemes(text)
|
gr := uniseg.NewGraphemes(text)
|
||||||
|
Loop:
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
|
st := style
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
if len(rs) == 1 && rs[0] == '\n' {
|
if len(rs) == 1 {
|
||||||
w.lastY++
|
r := rs[0]
|
||||||
w.lastX = 0
|
switch r {
|
||||||
lx = 0
|
case '\r':
|
||||||
continue
|
st = style.Dim(true)
|
||||||
|
rs[0] = '␍'
|
||||||
|
case '\n':
|
||||||
|
w.lastY++
|
||||||
|
w.lastX = 0
|
||||||
|
lx = 0
|
||||||
|
continue Loop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
@@ -643,8 +654,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
return FillSuspend
|
return FillSuspend
|
||||||
}
|
}
|
||||||
|
|
||||||
_screen.SetContent(xPos, yPos, rs[0], rs[1:], style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
lx += runewidth.StringWidth(string(rs))
|
lx += util.StringWidth(string(rs))
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
if w.lastX == w.width {
|
if w.lastX == w.width {
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ const (
|
|||||||
Start
|
Start
|
||||||
Load
|
Load
|
||||||
Focus
|
Focus
|
||||||
|
One
|
||||||
|
Zero
|
||||||
|
|
||||||
AltBS
|
AltBS
|
||||||
|
|
||||||
@@ -282,6 +284,15 @@ type Event struct {
|
|||||||
MouseEvent *MouseEvent
|
MouseEvent *MouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e Event) Is(types ...EventType) bool {
|
||||||
|
for _, t := range types {
|
||||||
|
if e.Type == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type MouseEvent struct {
|
type MouseEvent struct {
|
||||||
Y int
|
Y int
|
||||||
X int
|
X int
|
||||||
@@ -525,28 +536,28 @@ func EmptyTheme() *ColorTheme {
|
|||||||
func NoColorTheme() *ColorTheme {
|
func NoColorTheme() *ColorTheme {
|
||||||
return &ColorTheme{
|
return &ColorTheme{
|
||||||
Colored: false,
|
Colored: false,
|
||||||
Input: ColorAttr{colDefault, AttrRegular},
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
Fg: ColorAttr{colDefault, AttrRegular},
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Bg: ColorAttr{colDefault, AttrRegular},
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
DarkBg: ColorAttr{colDefault, AttrRegular},
|
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Prompt: ColorAttr{colDefault, AttrRegular},
|
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||||
Match: ColorAttr{colDefault, Underline},
|
Match: ColorAttr{colDefault, Underline},
|
||||||
Current: ColorAttr{colDefault, Reverse},
|
Current: ColorAttr{colDefault, Reverse},
|
||||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||||
Spinner: ColorAttr{colDefault, AttrRegular},
|
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||||
Info: ColorAttr{colDefault, AttrRegular},
|
Info: ColorAttr{colDefault, AttrUndefined},
|
||||||
Cursor: ColorAttr{colDefault, AttrRegular},
|
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||||
Selected: ColorAttr{colDefault, AttrRegular},
|
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||||
Header: ColorAttr{colDefault, AttrRegular},
|
Header: ColorAttr{colDefault, AttrUndefined},
|
||||||
Border: ColorAttr{colDefault, AttrRegular},
|
Border: ColorAttr{colDefault, AttrUndefined},
|
||||||
BorderLabel: ColorAttr{colDefault, AttrRegular},
|
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
Disabled: ColorAttr{colDefault, AttrRegular},
|
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||||
PreviewLabel: ColorAttr{colDefault, AttrRegular},
|
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
Separator: ColorAttr{colDefault, AttrRegular},
|
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||||
Scrollbar: ColorAttr{colDefault, AttrRegular},
|
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ import (
|
|||||||
"github.com/rivo/uniseg"
|
"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")
|
||||||
|
}
|
||||||
|
|
||||||
// RunesWidth returns runes width
|
// RunesWidth returns runes width
|
||||||
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||||
width := 0
|
width := 0
|
||||||
@@ -22,8 +27,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
|
|||||||
if len(rs) == 1 && rs[0] == '\t' {
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
w = tabstop - (prefixWidth+width)%tabstop
|
w = tabstop - (prefixWidth+width)%tabstop
|
||||||
} else {
|
} else {
|
||||||
s := string(rs)
|
w = StringWidth(string(rs))
|
||||||
w = runewidth.StringWidth(s) + strings.Count(s, "\n")
|
|
||||||
}
|
}
|
||||||
width += w
|
width += w
|
||||||
if width > limit {
|
if width > limit {
|
||||||
@@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) {
|
|||||||
gr := uniseg.NewGraphemes(input)
|
gr := uniseg.NewGraphemes(input)
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
w := runewidth.StringWidth(string(rs))
|
w := StringWidth(string(rs))
|
||||||
if width+w > limit {
|
if width+w > limit {
|
||||||
return runes, width
|
return runes, width
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func TestMin32(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContrain(t *testing.T) {
|
func TestConstrain(t *testing.T) {
|
||||||
if Constrain(-3, -1, 3) != -1 {
|
if Constrain(-3, -1, 3) != -1 {
|
||||||
t.Error("Expected", -1)
|
t.Error("Expected", -1)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func TestContrain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContrain32(t *testing.T) {
|
func TestConstrain32(t *testing.T) {
|
||||||
if Constrain32(-3, -1, 3) != -1 {
|
if Constrain32(-3, -1, 3) != -1 {
|
||||||
t.Error("Expected", -1)
|
t.Error("Expected", -1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes the given command with $SHELL
|
// ExecCommand executes the given command with $SHELL
|
||||||
@@ -45,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
|||||||
func Read(fd int, b []byte) (int, error) {
|
func Read(fd int, b []byte) (int, error) {
|
||||||
return syscall.Read(int(fd), b)
|
return syscall.Read(int(fd), b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetStdin(file *os.File) {
|
||||||
|
unix.Dup2(int(file.Fd()), 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
|||||||
func Read(fd int, b []byte) (int, error) {
|
func Read(fd int, b []byte) (int, error) {
|
||||||
return syscall.Read(syscall.Handle(fd), b)
|
return syscall.Read(syscall.Handle(fd), b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetStdin(file *os.File) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|||||||
420
test/test_go.rb
420
test/test_go.rb
@@ -180,7 +180,7 @@ class TestBase < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def writelines(path, lines)
|
def writelines(path, lines)
|
||||||
File.unlink(path) while File.exist?(path)
|
FileUtils.rm_f(path) while File.exist?(path)
|
||||||
File.open(path, 'w') { |f| f.puts lines }
|
File.open(path, 'w') { |f| f.puts lines }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ class TestBase < Minitest::Test
|
|||||||
wait { assert_path_exists tempname }
|
wait { assert_path_exists tempname }
|
||||||
File.read(tempname)
|
File.read(tempname)
|
||||||
ensure
|
ensure
|
||||||
File.unlink(tempname) while File.exist?(tempname)
|
FileUtils.rm_f(tempname) while File.exist?(tempname)
|
||||||
@temp_suffix += 1
|
@temp_suffix += 1
|
||||||
tmux.prepare
|
tmux.prepare
|
||||||
end
|
end
|
||||||
@@ -905,11 +905,7 @@ class TestGoFZF < TestBase
|
|||||||
history_file = '/tmp/fzf-test-history'
|
history_file = '/tmp/fzf-test-history'
|
||||||
|
|
||||||
# History with limited number of entries
|
# History with limited number of entries
|
||||||
begin
|
FileUtils.rm_f(history_file)
|
||||||
File.unlink(history_file)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
opts = "--history=#{history_file} --history-size=4"
|
opts = "--history=#{history_file} --history-size=4"
|
||||||
input = %w[00 11 22 33 44]
|
input = %w[00 11 22 33 44]
|
||||||
input.each do |keys|
|
input.each do |keys|
|
||||||
@@ -955,7 +951,7 @@ class TestGoFZF < TestBase
|
|||||||
tmux.until { |lines| assert_equal '> 33', lines[-1] }
|
tmux.until { |lines| assert_equal '> 33', lines[-1] }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
ensure
|
ensure
|
||||||
File.unlink(history_file)
|
FileUtils.rm_f(history_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_execute
|
def test_execute
|
||||||
@@ -984,11 +980,7 @@ class TestGoFZF < TestBase
|
|||||||
], File.readlines(output, chomp: true)
|
], File.readlines(output, chomp: true)
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_execute_multi
|
def test_execute_multi
|
||||||
@@ -1013,20 +1005,12 @@ class TestGoFZF < TestBase
|
|||||||
], File.readlines(output, chomp: true)
|
], File.readlines(output, chomp: true)
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_execute_plus_flag
|
def test_execute_plus_flag
|
||||||
output = tempname + '.tmp'
|
output = tempname + '.tmp'
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
writelines(tempname, ['foo bar', '123 456'])
|
writelines(tempname, ['foo bar', '123 456'])
|
||||||
|
|
||||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||||
@@ -1059,21 +1043,13 @@ class TestGoFZF < TestBase
|
|||||||
], File.readlines(output, chomp: true)
|
], File.readlines(output, chomp: true)
|
||||||
end
|
end
|
||||||
rescue StandardError
|
rescue StandardError
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_execute_shell
|
def test_execute_shell
|
||||||
# Custom script to use as $SHELL
|
# Custom script to use as $SHELL
|
||||||
output = tempname + '.out'
|
output = tempname + '.out'
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
writelines(tempname,
|
writelines(tempname,
|
||||||
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||||
system("chmod +x #{tempname}")
|
system("chmod +x #{tempname}")
|
||||||
@@ -1087,11 +1063,7 @@ class TestGoFZF < TestBase
|
|||||||
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
begin
|
FileUtils.rm_f(output)
|
||||||
File.unlink(output)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_cycle
|
def test_cycle
|
||||||
@@ -1485,6 +1457,83 @@ class TestGoFZF < TestBase
|
|||||||
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_toggle_preview_without_default_preview_command
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
refute_includes lines[1], '║ [1]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# toggle-preview should do nothing
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
refute_includes lines[1], '║ [1]'
|
||||||
|
refute_includes lines[1], '║ [2]'
|
||||||
|
end
|
||||||
|
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 3'
|
||||||
|
refute_includes lines[1], '║ [3]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# One-off preview action
|
||||||
|
tmux.send_keys :Space
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
|
||||||
|
|
||||||
|
# toggle-preview to hide it
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
|
||||||
|
|
||||||
|
# toggle-preview again does nothing
|
||||||
|
tmux.send_keys :Enter, :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 4'
|
||||||
|
refute_includes lines[1], '║ [4]'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_show_and_hide_preview
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
|
||||||
|
|
||||||
|
# Hidden by default
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 100, lines.match_count
|
||||||
|
refute_includes lines[1], '┃ [1]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
|
||||||
|
|
||||||
|
# Already shown
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
|
||||||
|
|
||||||
|
# Hide
|
||||||
|
tmux.send_keys :b
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 3'
|
||||||
|
refute_includes lines[1], '┃ [3]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Already hidden
|
||||||
|
tmux.send_keys :b
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines, '> 4'
|
||||||
|
refute_includes lines[1], '┃ [4]'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Show it again
|
||||||
|
tmux.send_keys :a
|
||||||
|
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
|
||||||
|
end
|
||||||
|
|
||||||
def test_preview_hidden
|
def test_preview_hidden
|
||||||
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
||||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
@@ -1497,11 +1546,7 @@ class TestGoFZF < TestBase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_preview_size_0
|
def test_preview_size_0
|
||||||
begin
|
FileUtils.rm_f(tempname)
|
||||||
File.unlink(tempname)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||||
tmux.until do |lines|
|
tmux.until do |lines|
|
||||||
assert_equal 100, lines.item_count
|
assert_equal 100, lines.item_count
|
||||||
@@ -1526,6 +1571,32 @@ class TestGoFZF < TestBase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_preview_size_0_hidden
|
||||||
|
FileUtils.rm_f(tempname)
|
||||||
|
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.send_keys :Down, :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 3' }
|
||||||
|
wait { refute_path_exists tempname }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait do
|
||||||
|
assert_path_exists tempname
|
||||||
|
assert_equal %w[3], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Down
|
||||||
|
wait do
|
||||||
|
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
tmux.send_keys :Space, :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||||
|
tmux.send_keys :Down
|
||||||
|
tmux.until { |lines| assert_includes lines, '> 6' }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
wait do
|
||||||
|
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_preview_flags
|
def test_preview_flags
|
||||||
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
||||||
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
||||||
@@ -1794,6 +1865,67 @@ class TestGoFZF < TestBase
|
|||||||
tmux.until { |lines| assert_equal '>', lines.last }
|
tmux.until { |lines| assert_equal '>', lines.last }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_change_and_transform_header
|
||||||
|
[
|
||||||
|
'space:change-header:$(seq 4)',
|
||||||
|
'space:transform-header:seq 4'
|
||||||
|
].each_with_index do |binding, i|
|
||||||
|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
bar
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, _1) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, _1) }
|
||||||
|
next unless i.zero?
|
||||||
|
|
||||||
|
teardown
|
||||||
|
setup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_change_header
|
||||||
|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
bar
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, _1) }
|
||||||
|
tmux.send_keys :Space
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
> 3
|
||||||
|
2
|
||||||
|
1
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
4
|
||||||
|
1/1
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, _1) }
|
||||||
|
end
|
||||||
|
|
||||||
def test_change_query
|
def test_change_query
|
||||||
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
|
||||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||||
@@ -1859,7 +1991,7 @@ class TestGoFZF < TestBase
|
|||||||
|
|
||||||
def test_keep_right
|
def test_keep_right
|
||||||
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
|
tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter
|
||||||
tmux.until { |lines| assert lines.any_include?('9999 10000') }
|
tmux.until { |lines| assert lines.any_include?('9999␊10000') }
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_backward_eof
|
def test_backward_eof
|
||||||
@@ -2087,11 +2219,7 @@ class TestGoFZF < TestBase
|
|||||||
wait { refute system("pgrep -f #{script}") }
|
wait { refute system("pgrep -f #{script}") }
|
||||||
ensure
|
ensure
|
||||||
system("pkill -9 -f #{script}")
|
system("pkill -9 -f #{script}")
|
||||||
begin
|
FileUtils.rm_f(script)
|
||||||
File.unlink(script)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_kill_default_command_on_accept
|
def test_kill_default_command_on_accept
|
||||||
@@ -2109,11 +2237,7 @@ class TestGoFZF < TestBase
|
|||||||
wait { refute system("pgrep -f #{script}") }
|
wait { refute system("pgrep -f #{script}") }
|
||||||
ensure
|
ensure
|
||||||
system("pkill -9 -f #{script}")
|
system("pkill -9 -f #{script}")
|
||||||
begin
|
FileUtils.rm_f(script)
|
||||||
File.unlink(script)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_kill_reload_command_on_abort
|
def test_kill_reload_command_on_abort
|
||||||
@@ -2134,11 +2258,7 @@ class TestGoFZF < TestBase
|
|||||||
wait { refute system("pgrep -f #{script}") }
|
wait { refute system("pgrep -f #{script}") }
|
||||||
ensure
|
ensure
|
||||||
system("pkill -9 -f #{script}")
|
system("pkill -9 -f #{script}")
|
||||||
begin
|
FileUtils.rm_f(script)
|
||||||
File.unlink(script)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_kill_reload_command_on_accept
|
def test_kill_reload_command_on_accept
|
||||||
@@ -2158,11 +2278,7 @@ class TestGoFZF < TestBase
|
|||||||
wait { refute system("pgrep -f #{script}") }
|
wait { refute system("pgrep -f #{script}") }
|
||||||
ensure
|
ensure
|
||||||
system("pkill -9 -f #{script}")
|
system("pkill -9 -f #{script}")
|
||||||
begin
|
FileUtils.rm_f(script)
|
||||||
File.unlink(script)
|
|
||||||
rescue StandardError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_preview_header
|
def test_preview_header
|
||||||
@@ -2574,11 +2690,17 @@ class TestGoFZF < TestBase
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_listen
|
def test_listen
|
||||||
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
|
{ '--listen 6266' => -> { URI('http://localhost:6266') },
|
||||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
|
||||||
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
|
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||||
|
teardown
|
||||||
|
setup
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_toggle_alternative_preview_window
|
def test_toggle_alternative_preview_window
|
||||||
@@ -2588,6 +2710,162 @@ class TestGoFZF < TestBase
|
|||||||
tmux.send_keys :Space
|
tmux.send_keys :Space
|
||||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_become
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
tmux.send_keys 999
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
tmux.until { |lines| assert_equal 99, lines.item_count }
|
||||||
|
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)
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
╭──────────
|
||||||
|
│ ─────────
|
||||||
|
│ something
|
||||||
|
│
|
||||||
|
╰──────────
|
||||||
|
3
|
||||||
|
2
|
||||||
|
> 1
|
||||||
|
100/100 ─
|
||||||
|
>
|
||||||
|
OUTPUT
|
||||||
|
tmux.until { assert_block(expected, _1) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_track
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> 555'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
index = tmux.until do |lines|
|
||||||
|
assert_equal 28, lines.match_count
|
||||||
|
assert_includes lines, '> 555'
|
||||||
|
end.index('> 555')
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 271, lines.match_count
|
||||||
|
assert_equal '> 555', lines[index]
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1000, lines.match_count
|
||||||
|
assert_equal '> 555', lines[index]
|
||||||
|
end
|
||||||
|
tmux.send_keys '555'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> 555'
|
||||||
|
assert_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys 't'
|
||||||
|
tmux.until do |lines|
|
||||||
|
refute_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 28, lines.match_count
|
||||||
|
assert_includes lines, '> 55'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 271, lines.match_count
|
||||||
|
assert_includes lines, '> 5'
|
||||||
|
end
|
||||||
|
tmux.send_keys 't'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1000, lines.match_count
|
||||||
|
assert_includes lines, '> 5'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_track_action
|
||||||
|
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_includes lines, '> 555'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 28, lines.match_count
|
||||||
|
assert_includes lines, '> 55'
|
||||||
|
end
|
||||||
|
tmux.send_keys :t
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 271, lines.match_count
|
||||||
|
assert_includes lines, '> 55'
|
||||||
|
end
|
||||||
|
|
||||||
|
# Automatically disabled when the tracking item is no longer visible
|
||||||
|
tmux.send_keys '4'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 28, lines.match_count
|
||||||
|
refute_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 271, lines.match_count
|
||||||
|
assert_includes lines, '> 5'
|
||||||
|
end
|
||||||
|
tmux.send_keys :t
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.until do |lines|
|
||||||
|
refute_includes lines[-2], '+T'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_one_and_zero
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
|
||||||
|
tmux.send_keys '1'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 2, lines.match_count
|
||||||
|
refute(lines.any? { _1.include?('only match') })
|
||||||
|
refute(lines.any? { _1.include?('no match') })
|
||||||
|
end
|
||||||
|
tmux.send_keys '0'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert(lines.any? { _1.include?('only match') })
|
||||||
|
end
|
||||||
|
tmux.send_keys '0'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 0, lines.match_count
|
||||||
|
assert(lines.any? { _1.include?('no match') })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_height_range_with_exit_0
|
||||||
|
tmux.send_keys "seq 10 | #{FZF} --height ~10% --exit-0", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
|
tmux.send_keys :c
|
||||||
|
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_reload_and_change
|
||||||
|
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
@@ -2716,9 +2994,9 @@ module TestShell
|
|||||||
tmux.send_keys 'C-r'
|
tmux.send_keys 'C-r'
|
||||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||||
tmux.send_keys 'foo bar'
|
tmux.send_keys 'foo bar'
|
||||||
tmux.until { |lines| assert lines[-3]&.end_with?('bar"') }
|
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert lines[-1]&.end_with?('bar"') }
|
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||||
tmux.send_keys :Enter
|
tmux.send_keys :Enter
|
||||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||||
end
|
end
|
||||||
|
|||||||
6
typos.toml
Normal file
6
typos.toml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos
|
||||||
|
[default.extend-words]
|
||||||
|
ba = "ba"
|
||||||
|
fo = "fo"
|
||||||
|
enew = "enew"
|
||||||
|
tabe = "tabe"
|
||||||
Reference in New Issue
Block a user