m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 06:13:47 -05:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Junegunn Choi
2024010119 0.45.0 2024-01-01 15:38:35 +09:00
Junegunn Choi
412040f77e Enable preview if 'transform' action is bound to a key 2023-12-31 20:56:33 +09:00
Junegunn Choi
d210660ce8 Add actions: show-header and hide-header 2023-12-31 16:01:00 +09:00
Junegunn Choi
863a12562b Trigger focus actions synchronously 2023-12-31 15:54:37 +09:00
junegunn
5da606a9ac Deploying to master from @ junegunn/fzf@8d20f3d5c4 🚀 2023-12-31 00:01:48 +00:00
Junegunn Choi
8d20f3d5c4 ADVANCED.md: Add toggling example with transform and {fzf:prompt}
Courtesy of @LangLangBart
2023-12-29 12:27:12 +09:00
Junegunn Choi
5d360180af Add {fzf:prompt} placeholder expression
Close #3354
2023-12-28 17:10:06 +09:00
Junegunn Choi
f0fbed6007 Fix RuboCop error 2023-12-27 01:33:34 +09:00
Junegunn Choi
519de7c833 Fix unexpected result of --tiebreak=end
See https://github.com/junegunn/fzf/issues/3255#issuecomment-1869580320
2023-12-26 23:42:14 +09:00
Junegunn Choi
97ccef1a04 {fzf:query} should trigger preview update
fzf --preview 'echo {fzf:query}'
    fzf --preview 'echo {q}'
2023-12-26 16:51:41 +09:00
Junegunn Choi
c4df0dd06e Add TRANSFORM ACTIONS section to man page 2023-12-26 12:21:06 +09:00
Junegunn Choi
cd114c6818 Change transform action to directly execute actions
To avoid filling up input channel for HTTP server
2023-12-26 10:15:53 +09:00
Junegunn Choi
1707b8cdba Add 'transform' action to conditionally perform a series of actions
'transform' action runs an external command that prints a series of
actions to perform.

  # Disallow selecting an empty line
  echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
    fzf --reverse --header 'Select one' \
        --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'

  # Move cursor past the empty line
  echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
    fzf --reverse --header 'Select one' \
        --bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
        --bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'

Close #3368
Close #2980
2023-12-26 00:14:05 +09:00
Junegunn Choi
41d4d70b98 Fix shell escaping for fish
Fix #3224
2023-12-25 17:35:44 +09:00
Junegunn Choi
0e999482cb Fix handling of empty ANSI color sequence
Fix #3320
2023-12-25 17:05:54 +09:00
junegunn
65b2c06027 Deploying to master from @ junegunn/fzf@d7b61ede07 🚀 2023-12-24 00:01:38 +00:00
Junegunn Choi
d7b61ede07 Add support for negative --height
fzf --height=-1

Close #3487
2023-12-21 18:42:23 +09:00
dependabot[bot]
87fc1c84b8 Bump actions/setup-go from 4 to 5 (#3537)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 10:50:47 +09:00
dependabot[bot]
d4b5f12383 Bump github/codeql-action from 2 to 3 (#3544)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 09:59:59 +09:00
junegunn
eb62b0d665 Deploying to master from @ junegunn/fzf@91387a741b 🚀 2023-12-17 00:01:44 +00:00
Jan Verbeek
91387a741b Terminate simple server success response with double CRLF (#3542)
The simple success case had only the status line plus a single CRLF,
and pedantic HTTP client implementations (`hyper`) stumbled over
this. A double CRLF makes it OK.

Fixes #3541.
2023-12-16 15:15:00 +09:00
Junegunn Choi
e8b34cb00d Clarification on accept-or-print-query vs. become 2023-12-14 23:13:52 +09:00
Alec Scott
82954258c1 Add Spack installation instructions to README (#3526) 2023-12-10 16:08:54 +09:00
Junegunn Choi
50f092551b Lint: RuboCop 2023-12-10 16:04:30 +09:00
Junegunn Choi
c36a64be68 Add accept-or-print-query
Close #3528
2023-12-10 15:59:45 +09:00
dependabot[bot]
a343b20775 Bump golang.org/x/term from 0.13.0 to 0.15.0 (#3525)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.13.0 to 0.15.0.
- [Commits](https://github.com/golang/term/compare/v0.13.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-10 15:16:47 +09:00
junegunn
a714e76ae1 Deploying to master from @ junegunn/fzf@d21d5c9510 🚀 2023-12-10 00:01:46 +00:00
junegunn
d21d5c9510 Deploying to master from @ junegunn/fzf@cd6788a2bb 🚀 2023-12-03 00:01:38 +00:00
Junegunn Choi
cd6788a2bb Increase buffer size of event channel to avoid freeze on zero event
Fix #3516
2023-11-30 17:23:46 +09:00
junegunn
6b99399c41 Deploying to master from @ junegunn/fzf@952b6af445 🚀 2023-11-26 00:01:42 +00:00
Laurent Cheylus
952b6af445 Allow files creation in /tmp on OpenBSD (#3512)
- src/protector/protector_openbsd.go: add tmppath for pledge
    permissions
  - fix junegunn/fzf#3511

Signed-off-by: Laurent Cheylus <foxy@free.fr>
2023-11-21 16:03:12 +09:00
junegunn
7c674ad7fa Deploying to master from @ junegunn/fzf@d7d2ac3951 🚀 2023-11-19 00:01:41 +00:00
31 changed files with 600 additions and 132 deletions

View File

@@ -33,12 +33,12 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: 1.19 go-version: 1.19

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version: 1.18 go-version: 1.18

View File

@@ -1,8 +1,8 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2023/05/26* * *Last update: 2023/12/29*
* *Requires fzf 0.41.0 or above* * *Requires fzf 0.45.0 or above*
--- ---
@@ -16,6 +16,7 @@ Advanced fzf examples
* [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)
* [Toggling with a single key binding](#toggling-with-a-single-key-binding)
* [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)
@@ -208,6 +209,30 @@ find * | fzf --prompt 'All> ' \
![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png) ![image](https://user-images.githubusercontent.com/700826/113465072-46321c00-946c-11eb-9b6f-cda3951df579.png)
### Toggling with a single key binding
The above example uses two different key bindings to toggle between two modes,
but can we just use a single key binding?
To make a key binding behave differently each time it is pressed, we need:
1. a way to store the current state. i.e. "which mode are we in?"
2. and a way to dynamically perform different actions depending on the state.
The following example shows how to 1. store the current mode in the prompt
string, 2. and use this information (`{fzf:prompt}`) to determine which
actions to perform using the `transform` action.
```sh
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
--preview '[[ {fzf:prompt} =~ Files ]] && bat --color=always {} || tree -C {}'
```
Ripgrep integration Ripgrep integration
------------------- -------------------

View File

@@ -1,6 +1,56 @@
CHANGELOG CHANGELOG
========= =========
0.45.0
------
- Added `transform` action to conditionally perform a series of actions
```sh
# Disallow selecting an empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"'
# Move cursor past the empty line
echo -e "1. Hello\n2. Goodbye\n\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \
--bind 'enter:transform:[[ -n {} ]] && echo accept || echo "change-header:Invalid selection"' \
--bind 'focus:transform:[[ -n {} ]] && exit; [[ {fzf:action} =~ up$ ]] && echo up || echo down'
# A single key binding to toggle between modes
fd --type file |
fzf --prompt 'Files> ' \
--header 'CTRL-T: Switch between Files/Directories' \
--bind 'ctrl-t:transform:[[ ! {fzf:prompt} =~ Files ]] &&
echo "change-prompt(Files> )+reload(fd --type file)" ||
echo "change-prompt(Directories> )+reload(fd --type directory)"'
```
- Added placeholder expressions
- `{fzf:action}` - The name of the last action performed
- `{fzf:prompt}` - Prompt string (including ANSI color codes)
- `{fzf:query}` - Synonym for `{q}`
- Added support for negative height
```sh
# Terminal height minus 1, so you can still see the command line
fzf --height=-1
```
- This handles a terminal resize better than `--height=$(($(tput lines) - 1))`
- Added `accept-or-print-query` action that acts like `accept` but prints the
current query when there's no match for the query
```sh
# You can make CTRL-R paste the current query when there's no match
export FZF_CTRL_R_OPTS='--bind enter:accept-or-print-query'
```
- Note that there are alternative ways to implement the same strategy
```sh
# 'become' is apparently more versatile but it's not available on Windows.
export FZF_CTRL_R_OPTS='--bind "enter:become:if [ -z {} ]; then echo {q}; else echo {}; fi"'
# Using the new 'transform' action
export FZF_CTRL_R_OPTS='--bind "enter:transform:[ -z {} ] && echo print-query || echo accept"'
```
- Added `show-header` and `hide-header` actions
- Bug fixes
0.44.1 0.44.1
------ ------
- Fixed crash when preview window is hidden on `focus` event - Fixed crash when preview window is hidden on `focus` event

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -89,6 +89,9 @@ bench:
install: bin/fzf install: bin/fzf
generate:
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
build: build:
goreleaser build --rm-dist --snapshot --skip-post-hooks goreleaser build --rm-dist --snapshot --skip-post-hooks
@@ -181,4 +184,4 @@ update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all build release test bench install clean docker docker-test update .PHONY: all generate build release test bench install clean docker docker-test update

View File

@@ -489,4 +489,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
fzf.txt fzf Last change: September 17 2023 fzf.txt fzf Last change: January 1 2024
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -512,7 +512,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

8
go.mod
View File

@@ -7,15 +7,17 @@ require (
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.4 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.14.0 golang.org/x/sys v0.15.0
golang.org/x/term v0.13.0 golang.org/x/term v0.15.0
) )
require ( require (
github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
) )
go 1.17 go 1.17

15
go.sum
View File

@@ -19,24 +19,29 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
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=
@@ -45,4 +50,6 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.44.1 version=0.45.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.44.1" $version="0.45.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.44" var version string = "0.45"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Nov 2023" "fzf 0.44.1" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jan 2024" "fzf 0.45.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

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Nov 2023" "fzf 0.44.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2024" "fzf 0.45.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -192,9 +192,21 @@ Label characters for \fBjump\fR and \fBjump-accept\fR
.TP .TP
.BI "--height=" "[~]HEIGHT[%]" .BI "--height=" "[~]HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using Display fzf window below the cursor with the given height instead of using
the full screen. When prefixed with \fB~\fR, fzf will automatically determine the full screen.
the height in the range according to the input size. Note that adaptive height
is not compatible with top/bottom margin and padding given in percent size. If a negative value is specified, the height is calculated as the terminal
height minus the given value.
fzf --height=-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the
range according to the input size. Note that adaptive height is not compatible
with top/bottom margin and padding given in percent size. It is also not
compatible with a negative height value.
# Will not take up 100% of the screen
seq 5 | fzf --height=~100%
.TP .TP
.BI "--min-height=" "HEIGHT" .BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10). Minimum height when \fB--height\fR is given in percent (default: 10).
@@ -562,10 +574,6 @@ e.g.
When using a field index expression, leading and trailing whitespace is stripped When using a field index expression, leading and trailing whitespace is stripped
from the replacement string. To preserve the whitespace, use the \fBs\fR flag. from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
all index numbers when multiple lines are selected.
A placeholder expression with \fBf\fR flag is replaced to the path of A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may multi-select a large number of items and the length of the evaluated string may
@@ -577,6 +585,16 @@ e.g.
seq 100000 | fzf --multi --bind ctrl-a:select-all \\ seq 100000 | fzf --multi --bind ctrl-a:select-all \\
--preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR --preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
Also,
* \fB{q}\fR (or \fB{fzf:query}\fR) is replaced to the current query string
.br
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
.br
* \fB{fzf:action}\fR is replaced to to the name of the last action performed
* \fB{fzf:prompt}\fR is replaced to to the prompt string
Note that you can escape a placeholder pattern by prepending a backslash. Note that you can escape a placeholder pattern by prepending a backslash.
Preview window will be updated even when there is no match for the current Preview window will be updated even when there is no match for the current
@@ -1120,6 +1138,7 @@ A key or an event can be bound to one or more of the following actions.
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBaccept-or-print-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
@@ -1164,6 +1183,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-header\fR
\fBhide-preview\fR \fBhide-preview\fR
\fBoffset-down\fR (similar to CTRL-E of Vim) \fBoffset-down\fR (similar to CTRL-E of Vim)
\fBoffset-up\fR (similar to CTRL-Y of Vim) \fBoffset-up\fR (similar to CTRL-Y of Vim)
@@ -1189,6 +1209,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-header\fR
\fBshow-preview\fR \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)
@@ -1203,6 +1224,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-track\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) \fBtrack\fR (track the current item; automatically disabled if focus changes)
\fBtransform(...)\fR (transform states using the output of an external command)
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header 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)
@@ -1315,6 +1337,28 @@ e.g.
\fB# You can still filter and select entries from the initial list for 3 seconds \fB# You can still filter and select entries from the initial list for 3 seconds
seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR seq 100 | fzf --bind 'load:reload-sync(sleep 3; seq 1000)+unbind(load)'\fR
.SS TRANSFORM ACTIONS
Actions with \fBtransform-\fR prefix are used to transform the states of fzf
using the output of an external command. The output of these commands are
expected to be a single line of text.
e.g.
\fBfzf --bind 'focus:transform-header:file --brief {}'\fR
\fBtransform(...)\fR action runs an external command that should print a series
of actions to be performed. The output should be in the same format as the
payload of HTTP POST request to the \fB--listen\fR server.
e.g.
\fB# Disallow selecting an empty line
echo -e "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf --height '~100%' --reverse --header 'Select one' \\
--bind 'enter:transform:[[ -n {} ]] &&
echo accept ||
echo "change-header:Invalid selection"'
\fR
.SS PREVIEW BINDING .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2023 Junegunn Choi " Copyright (c) 2013-2024 Junegunn Choi
" "
" MIT License " MIT License
" "

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2023 Junegunn Choi Copyright (c) 2013-2024 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

129
src/actiontype_string.go Normal file
View File

@@ -0,0 +1,129 @@
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
package fzf
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[actIgnore-0]
_ = x[actStart-1]
_ = x[actClick-2]
_ = x[actInvalid-3]
_ = x[actChar-4]
_ = x[actMouse-5]
_ = x[actBeginningOfLine-6]
_ = x[actAbort-7]
_ = x[actAccept-8]
_ = x[actAcceptNonEmpty-9]
_ = x[actAcceptOrPrintQuery-10]
_ = x[actBackwardChar-11]
_ = x[actBackwardDeleteChar-12]
_ = x[actBackwardDeleteCharEof-13]
_ = x[actBackwardWord-14]
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleHeader-46]
_ = x[actTrack-47]
_ = x[actDown-48]
_ = x[actUp-49]
_ = x[actPageUp-50]
_ = x[actPageDown-51]
_ = x[actPosition-52]
_ = x[actHalfPageUp-53]
_ = x[actHalfPageDown-54]
_ = x[actOffsetUp-55]
_ = x[actOffsetDown-56]
_ = x[actJump-57]
_ = x[actJumpAccept-58]
_ = x[actPrintQuery-59]
_ = x[actRefreshPreview-60]
_ = x[actReplaceQuery-61]
_ = x[actToggleSort-62]
_ = x[actShowPreview-63]
_ = x[actHidePreview-64]
_ = x[actTogglePreview-65]
_ = x[actTogglePreviewWrap-66]
_ = x[actTransform-67]
_ = x[actTransformBorderLabel-68]
_ = x[actTransformHeader-69]
_ = x[actTransformPreviewLabel-70]
_ = x[actTransformPrompt-71]
_ = x[actTransformQuery-72]
_ = x[actPreview-73]
_ = x[actChangePreview-74]
_ = x[actChangePreviewWindow-75]
_ = x[actPreviewTop-76]
_ = x[actPreviewBottom-77]
_ = x[actPreviewUp-78]
_ = x[actPreviewDown-79]
_ = x[actPreviewPageUp-80]
_ = x[actPreviewPageDown-81]
_ = x[actPreviewHalfPageUp-82]
_ = x[actPreviewHalfPageDown-83]
_ = x[actPrevHistory-84]
_ = x[actPrevSelected-85]
_ = x[actPut-86]
_ = x[actNextHistory-87]
_ = x[actNextSelected-88]
_ = x[actExecute-89]
_ = x[actExecuteSilent-90]
_ = x[actExecuteMulti-91]
_ = x[actSigStop-92]
_ = x[actFirst-93]
_ = x[actLast-94]
_ = x[actReload-95]
_ = x[actReloadSync-96]
_ = x[actDisableSearch-97]
_ = x[actEnableSearch-98]
_ = x[actSelect-99]
_ = x[actDeselect-100]
_ = x[actUnbind-101]
_ = x[actRebind-102]
_ = x[actBecome-103]
_ = x[actResponse-104]
_ = x[actShowHeader-105]
_ = x[actHideHeader-106]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
}

View File

@@ -351,9 +351,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0 var delimiter byte = 0
count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 { if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
count++
switch state256 { switch state256 {
case 0: case 0:
switch num { switch num {
@@ -435,6 +437,13 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
} }
} }
// Empty sequence: reset
if count == 0 {
state.fg = -1
state.bg = -1
state.attr = 0
}
if state256 > 0 { if state256 > 0 {
*ptr = -1 *ptr = -1
} }

View File

@@ -348,6 +348,9 @@ func TestAnsiCodeStringConversion(t *testing.T) {
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "") assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
assert("\x1b[31m", nil, "\x1b[31;49m") assert("\x1b[31m", nil, "\x1b[31;49m")
assert("\x1b[41m", nil, "\x1b[39;41m") assert("\x1b[41m", nil, "\x1b[39;41m")

View File

@@ -200,7 +200,7 @@ func Run(opts *Options, version string, revision string) {
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
if heightUnknown { if heightUnknown {
maxFit, padHeight = terminal.MaxFitAndPad(opts) maxFit, padHeight = terminal.MaxFitAndPad()
} }
deferred := opts.Select1 || opts.Exit0 deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()

View File

@@ -57,6 +57,8 @@ const usage = `usage: fzf [options]
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calcalated as the terminal height
minus the given value.
If prefixed with '~', fzf will determine the height If prefixed with '~', fzf will determine the height
according to the input size. according to the input size.
--min-height=HEIGHT Minimum height when --height is given in percent --min-height=HEIGHT Minimum height when --height is given in percent
@@ -157,6 +159,7 @@ type heightSpec struct {
size float64 size float64
percent bool percent bool
auto bool auto bool
inverse bool
} }
type sizeSpec struct { type sizeSpec struct {
@@ -976,7 +979,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1070,6 +1073,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actAccept) appendAction(actAccept)
case "accept-non-empty": case "accept-non-empty":
appendAction(actAcceptNonEmpty) appendAction(actAcceptNonEmpty)
case "accept-or-print-query":
appendAction(actAcceptOrPrintQuery)
case "print-query": case "print-query":
appendAction(actPrintQuery) appendAction(actPrintQuery)
case "refresh-preview": case "refresh-preview":
@@ -1081,7 +1086,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "backward-delete-char": case "backward-delete-char":
appendAction(actBackwardDeleteChar) appendAction(actBackwardDeleteChar)
case "backward-delete-char/eof": case "backward-delete-char/eof":
appendAction(actBackwardDeleteCharEOF) appendAction(actBackwardDeleteCharEof)
case "backward-word": case "backward-word":
appendAction(actBackwardWord) appendAction(actBackwardWord)
case "clear-screen": case "clear-screen":
@@ -1089,7 +1094,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
case "delete-char": case "delete-char":
appendAction(actDeleteChar) appendAction(actDeleteChar)
case "delete-char/eof": case "delete-char/eof":
appendAction(actDeleteCharEOF) appendAction(actDeleteCharEof)
case "deselect": case "deselect":
appendAction(actDeselect) appendAction(actDeselect)
case "end-of-line": case "end-of-line":
@@ -1136,6 +1141,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleTrack) appendAction(actToggleTrack)
case "toggle-header": case "toggle-header":
appendAction(actToggleHeader) appendAction(actToggleHeader)
case "show-header":
appendAction(actShowHeader)
case "hide-header":
appendAction(actHideHeader)
case "track": case "track":
appendAction(actTrack) appendAction(actTrack)
case "select": case "select":
@@ -1208,7 +1217,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actDisableSearch) appendAction(actDisableSearch)
case "put": case "put":
if putAllowed { if putAllowed {
appendAction(actRune) appendAction(actChar)
} else { } else {
exit("unable to put non-printable character") exit("unable to put non-printable character")
} }
@@ -1328,6 +1337,8 @@ func isExecuteAction(str string) actionType {
return actExecuteMulti return actExecuteMulti
case "put": case "put":
return actPut return actPut
case "transform":
return actTransform
case "transform-border-label": case "transform-border-label":
return actTransformBorderLabel return actTransformBorderLabel
case "transform-preview-label": case "transform-preview-label":
@@ -1384,6 +1395,13 @@ func parseHeight(str string) heightSpec {
heightSpec.auto = true heightSpec.auto = true
str = str[1:] str = str[1:]
} }
if strings.HasPrefix(str, "-") {
if heightSpec.auto {
errorExit("negative(-) height is not compatible with adaptive(~) height")
}
heightSpec.inverse = true
str = str[1:]
}
size := parseSize(str, 100, "height") size := parseSize(str, 100, "height")
heightSpec.size = size.size heightSpec.size = size.size

View File

@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {
unix.PledgePromises("stdio rpath tty proc exec inet") unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
} }

View File

@@ -80,7 +80,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
if criterion == byBegin { if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen) val = util.AsUint16(minEnd - whitePrefixLen)
} else { } else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
} }
} }
} }

View File

@@ -218,7 +218,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
} }
server.actionChannel <- actions server.actionChannel <- actions
return httpOk return httpOk + crlf
} }
func parseGetParams(query string) getParams { func parseGetParams(query string) getParams {

View File

@@ -52,11 +52,12 @@ var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string var activeTempFiles []string
var passThroughRegex *regexp.Regexp var passThroughRegex *regexp.Regexp
var actionTypeRegex *regexp.Regexp
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
func init() { func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`) placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
whiteSuffix = regexp.MustCompile(`\s*$`) whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`) offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`) offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
@@ -182,6 +183,7 @@ type Terminal struct {
separator labelPrinter separator labelPrinter
separatorLen int separatorLen int
spinner []string spinner []string
promptString string
prompt func() prompt func()
promptLen int promptLen int
borderLabel labelPrinter borderLabel labelPrinter
@@ -285,6 +287,8 @@ type Terminal struct {
tui tui.Renderer tui tui.Renderer
executing *util.AtomicBool executing *util.AtomicBool
termSize tui.TermSize termSize tui.TermSize
lastAction actionType
lastFocus int32
} }
type selectedItem struct { type selectedItem struct {
@@ -332,20 +336,24 @@ type action struct {
a string a string
} }
//go:generate stringer -type=actionType
type actionType int type actionType int
const ( const (
actIgnore actionType = iota actIgnore actionType = iota
actStart
actClick
actInvalid actInvalid
actRune actChar
actMouse actMouse
actBeginningOfLine actBeginningOfLine
actAbort actAbort
actAccept actAccept
actAcceptNonEmpty actAcceptNonEmpty
actAcceptOrPrintQuery
actBackwardChar actBackwardChar
actBackwardDeleteChar actBackwardDeleteChar
actBackwardDeleteCharEOF actBackwardDeleteCharEof
actBackwardWord actBackwardWord
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
@@ -358,7 +366,7 @@ const (
actClearSelection actClearSelection
actClose actClose
actDeleteChar actDeleteChar
actDeleteCharEOF actDeleteCharEof
actEndOfLine actEndOfLine
actForwardChar actForwardChar
actForwardWord actForwardWord
@@ -399,6 +407,7 @@ const (
actHidePreview actHidePreview
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actTransform
actTransformBorderLabel actTransformBorderLabel
actTransformHeader actTransformHeader
actTransformPreviewLabel actTransformPreviewLabel
@@ -436,17 +445,21 @@ const (
actRebind actRebind
actBecome actBecome
actResponse actResponse
actShowHeader
actHideHeader
) )
func processExecution(action actionType) bool { func processExecution(action actionType) bool {
switch action { switch action {
case actTransformBorderLabel, case actTransform,
actTransformBorderLabel,
actTransformHeader, actTransformHeader,
actTransformPreviewLabel, actTransformPreviewLabel,
actTransformPrompt, actTransformPrompt,
actTransformQuery, actTransformQuery,
actPreview, actPreview,
actChangePreview, actChangePreview,
actRefreshPreview,
actExecute, actExecute,
actExecuteSilent, actExecuteSilent,
actExecuteMulti, actExecuteMulti,
@@ -462,7 +475,7 @@ type placeholderFlags struct {
plus bool plus bool
preserveSpace bool preserveSpace bool
number bool number bool
query bool forceUpdate bool
file bool file bool
} }
@@ -513,7 +526,7 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.CtrlG, actAbort) add(tui.CtrlG, actAbort)
add(tui.CtrlQ, actAbort) add(tui.CtrlQ, actAbort)
add(tui.ESC, actAbort) add(tui.ESC, actAbort)
add(tui.CtrlD, actDeleteCharEOF) add(tui.CtrlD, actDeleteCharEof)
add(tui.CtrlE, actEndOfLine) add(tui.CtrlE, actEndOfLine)
add(tui.CtrlF, actForwardChar) add(tui.CtrlF, actForwardChar)
add(tui.CtrlH, actBackwardDeleteChar) add(tui.CtrlH, actBackwardDeleteChar)
@@ -555,7 +568,7 @@ func defaultKeymap() map[tui.Event][]*action {
add(tui.SDown, actPreviewDown) add(tui.SDown, actPreviewDown)
add(tui.Mouse, actMouse) add(tui.Mouse, actMouse)
add(tui.LeftClick, actIgnore) add(tui.LeftClick, actClick)
add(tui.RightClick, actToggle) add(tui.RightClick, actToggle)
add(tui.SLeftClick, actToggle) add(tui.SLeftClick, actToggle)
add(tui.SRightClick, actToggle) add(tui.SRightClick, actToggle)
@@ -574,10 +587,14 @@ func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1)) return []rune(strings.Replace(query, "\t", " ", -1))
} }
func hasPreviewAction(opts *Options) bool { func mayTriggerPreview(opts *Options) bool {
if opts.ListenAddr != nil {
return true
}
for _, actions := range opts.Keymap { for _, actions := range opts.Keymap {
for _, action := range actions { for _, action := range actions {
if action.t == actPreview || action.t == actChangePreview { switch action.t {
case actPreview, actChangePreview, actTransform:
return true return true
} }
} }
@@ -593,10 +610,17 @@ func makeSpinner(unicode bool) []string {
} }
func evaluateHeight(opts *Options, termHeight int) int { func evaluateHeight(opts *Options, termHeight int) int {
size := opts.Height.size
if opts.Height.percent { if opts.Height.percent {
return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) if opts.Height.inverse {
size = 100 - size
}
return util.Max(int(size*float64(termHeight)/100.0), opts.MinHeight)
} }
return int(opts.Height.size) if opts.Height.inverse {
size = float64(termHeight) - size
}
return int(size)
} }
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
@@ -609,8 +633,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delay = initialDelay delay = initialDelay
} }
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 the previewer even when --preview option is not specified
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenAddr != nil { // * if HTTP server is enabled
// * if 'preview' or 'change-preview' action is bound to a key
// * if 'transform' action is bound to a key
if len(opts.Preview.command) > 0 || mayTriggerPreview(opts) {
previewBox = util.NewEventBox() previewBox = util.NewEventBox()
} }
var renderer tui.Renderer var renderer tui.Renderer
@@ -654,6 +681,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
infoSep: opts.InfoSep, infoSep: opts.InfoSep,
separator: nil, separator: nil,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
promptString: opts.Prompt,
queryLen: [2]int{0, 0}, queryLen: [2]int{0, 0},
layout: opts.Layout, layout: opts.Layout,
fullscreen: fullscreen, fullscreen: fullscreen,
@@ -729,10 +757,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
killChan: make(chan int), killChan: make(chan int),
serverInputChan: make(chan []*action, 10), serverInputChan: make(chan []*action, 10),
serverOutputChan: make(chan string), serverOutputChan: make(chan string),
eventChan: make(chan tui.Event, 1), eventChan: make(chan tui.Event, 3), // load / zero|one | GetChar
tui: renderer, tui: renderer,
initFunc: func() { renderer.Init() }, initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)} executing: util.NewAtomicBool(false),
lastAction: actStart,
lastFocus: minItem.Index()}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt) t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0) t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0) t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -818,7 +848,7 @@ func (t *Terminal) extraLines() int {
return extra return extra
} }
func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) { func (t *Terminal) MaxFitAndPad() (int, int) {
_, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() _, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2] padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
fit := screenHeight - padHeight - t.extraLines() fit := screenHeight - padHeight - t.extraLines()
@@ -2336,6 +2366,12 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return true, match[1:], flags return true, match[1:], flags
} }
if strings.HasPrefix(match, "{fzf:") {
// {fzf:*} are not determined by the current item
flags.forceUpdate = true
return false, match, flags
}
skipChars := 1 skipChars := 1
for _, char := range match[1:] { for _, char := range match[1:] {
switch char { switch char {
@@ -2352,7 +2388,7 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
flags.file = true flags.file = true
skipChars++ skipChars++
case 'q': case 'q':
flags.query = true flags.forceUpdate = true
// query flag is not skipped // query flag is not skipped
default: default:
break break
@@ -2364,14 +2400,14 @@ func parsePlaceholder(match string) (bool, string, placeholderFlags) {
return false, matchWithoutFlags, flags return false, matchWithoutFlags, flags
} }
func hasPreviewFlags(template string) (slot bool, plus bool, query bool) { func hasPreviewFlags(template string) (slot bool, plus bool, forceUpdate bool) {
for _, match := range placeholder.FindAllString(template, -1) { for _, match := range placeholder.FindAllString(template, -1) {
_, _, flags := parsePlaceholder(match) _, _, flags := parsePlaceholder(match)
if flags.plus { if flags.plus {
plus = true plus = true
} }
if flags.query { if flags.forceUpdate {
query = true forceUpdate = true
} }
slot = true slot = true
} }
@@ -2398,9 +2434,30 @@ func cleanTemporaryFiles() {
activeTempFiles = []string{} activeTempFiles = []string{}
} }
type replacePlaceholderParams struct {
template string
stripAnsi bool
delimiter Delimiter
printsep string
forcePlus bool
query string
allItems []*Item
lastAction actionType
prompt string
}
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
return replacePlaceholder( return replacePlaceholder(replacePlaceholderParams{
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list) template: template,
stripAnsi: t.ansi,
delimiter: t.delimiter,
printsep: t.printsep,
forcePlus: forcePlus,
query: input,
allItems: list,
lastAction: t.lastAction,
prompt: t.promptString,
})
} }
func (t *Terminal) evaluateScrollOffset() int { func (t *Terminal) evaluateScrollOffset() int {
@@ -2438,9 +2495,9 @@ func (t *Terminal) evaluateScrollOffset() int {
return util.Max(0, base) return util.Max(0, base)
} }
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholder(params replacePlaceholderParams) string {
current := allItems[:1] current := params.allItems[:1]
selected := allItems[1:] selected := params.allItems[1:]
if current[0] == nil { if current[0] == nil {
current = []*Item{} current = []*Item{}
} }
@@ -2449,7 +2506,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
} }
// replace placeholders one by one // replace placeholders one by one
return placeholder.ReplaceAllStringFunc(template, func(match string) string { return placeholder.ReplaceAllStringFunc(params.template, func(match string) string {
escaped, match, flags := parsePlaceholder(match) escaped, match, flags := parsePlaceholder(match)
// this function implements the effects a placeholder has on items // this function implements the effects a placeholder has on items
@@ -2459,8 +2516,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
switch { switch {
case escaped: case escaped:
return match return match
case match == "{q}": case match == "{q}" || match == "{fzf:query}":
return quoteEntry(query) return quoteEntry(params.query)
case match == "{}": case match == "{}":
replace = func(item *Item) string { replace = func(item *Item) string {
switch { switch {
@@ -2471,11 +2528,22 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
} }
return strconv.Itoa(n) return strconv.Itoa(n)
case flags.file: case flags.file:
return item.AsString(stripAnsi) return item.AsString(params.stripAnsi)
default: default:
return quoteEntry(item.AsString(stripAnsi)) return quoteEntry(item.AsString(params.stripAnsi))
} }
} }
case match == "{fzf:action}":
name := ""
for i, r := range params.lastAction.String()[3:] {
if i > 0 && r >= 'A' && r <= 'Z' {
name += "-"
}
name += string(r)
}
return strings.ToLower(name)
case match == "{fzf:prompt}":
return quoteEntry(params.prompt)
default: default:
// token type and also failover (below) // token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",") rangeExpressions := strings.Split(match[1:len(match)-1], ",")
@@ -2490,15 +2558,15 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
} }
replace = func(item *Item) string { replace = func(item *Item) string {
tokens := Tokenize(item.AsString(stripAnsi), delimiter) tokens := Tokenize(item.AsString(params.stripAnsi), params.delimiter)
trans := Transform(tokens, ranges) trans := Transform(tokens, ranges)
str := joinTokens(trans) str := joinTokens(trans)
// trim the last delimiter // trim the last delimiter
if delimiter.str != nil { if params.delimiter.str != nil {
str = strings.TrimSuffix(str, *delimiter.str) str = strings.TrimSuffix(str, *params.delimiter.str)
} else if delimiter.regex != nil { } else if params.delimiter.regex != nil {
delims := delimiter.regex.FindAllStringIndex(str, -1) delims := params.delimiter.regex.FindAllStringIndex(str, -1)
// make sure the delimiter is at the very end of the string // make sure the delimiter is at the very end of the string
if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { if len(delims) > 0 && delims[len(delims)-1][1] == len(str) {
str = str[:delims[len(delims)-1][0]] str = str[:delims[len(delims)-1][0]]
@@ -2518,7 +2586,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
// apply 'replace' function over proper set of items and return result // apply 'replace' function over proper set of items and return result
items := current items := current
if flags.plus || forcePlus { if flags.plus || params.forcePlus {
items = selected items = selected
} }
replacements := make([]string, len(items)) replacements := make([]string, len(items))
@@ -2528,7 +2596,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
} }
if flags.file { if flags.file {
return writeTemporaryFile(replacements, printsep) return writeTemporaryFile(replacements, params.printsep)
} }
return strings.Join(replacements, " ") return strings.Join(replacements, " ")
}) })
@@ -2610,8 +2678,8 @@ func (t *Terminal) currentItem() *Item {
func (t *Terminal) buildPlusList(template string, forcePlus 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, forceUpdate := hasPreviewFlags(template)
if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) { if !(!slot || forceUpdate || (forcePlus || plus) && len(t.selected) > 0) {
return current != nil, []*Item{current, current} return current != nil, []*Item{current, current}
} }
@@ -2705,6 +2773,13 @@ func (t *Terminal) pwindowSize() tui.TermSize {
return size return size
} }
func (t *Terminal) currentIndex() int32 {
if currentItem := t.currentItem(); currentItem != nil {
return currentItem.Index()
}
return minItem.Index()
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
// prof := profile.Start(profile.ProfilePath("/tmp/")) // prof := profile.Start(profile.ProfilePath("/tmp/"))
@@ -2989,17 +3064,14 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
var currentIndex int32 = minItem.Index() currentIndex := t.currentIndex()
currentItem := t.currentItem()
if currentItem != nil {
currentIndex = currentItem.Index()
}
focusChanged := focusedIndex != currentIndex focusChanged := focusedIndex != currentIndex
if focusChanged && t.track == trackCurrent { if focusChanged && t.track == trackCurrent {
t.track = trackDisabled t.track = trackDisabled
t.printInfo() t.printInfo()
} }
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged { if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged && currentIndex != t.lastFocus {
t.lastFocus = focusedIndex
t.serverInputChan <- onFocus t.serverInputChan <- onFocus
} }
if focusChanged || version != t.version { if focusChanged || version != t.version {
@@ -3189,17 +3261,26 @@ func (t *Terminal) Loop() {
} }
var doAction func(*action) bool var doAction func(*action) bool
doActions := func(actions []*action) bool { var doActions func(actions []*action) bool
doActions = func(actions []*action) bool {
currentIndex := t.currentIndex()
for _, action := range actions { for _, action := range actions {
if !doAction(action) { if !doAction(action) {
return false return false
} }
} }
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs {
if newIndex := t.currentIndex(); newIndex != currentIndex {
t.lastFocus = newIndex
return doActions(onFocus)
}
}
return true return true
} }
doAction = func(a *action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore, actStart, actClick:
case actResponse: case actResponse:
t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a)) t.serverOutputChan <- t.dumpStatus(parseGetParams(a.a))
case actBecome: case actBecome:
@@ -3270,6 +3351,7 @@ func (t *Terminal) Loop() {
} }
case actTransformPrompt: case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true, true) prompt := t.executeCommand(a.a, false, true, true, true)
t.promptString = prompt
t.prompt, t.promptLen = t.parsePrompt(prompt) t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt) req(reqPrompt)
case actTransformQuery: case actTransformQuery:
@@ -3346,6 +3428,10 @@ func (t *Terminal) Loop() {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actTransform:
body := t.executeCommand(a.a, false, true, true, false)
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
return doActions(actions)
case actTransformBorderLabel: case actTransformBorderLabel:
if t.border != nil { if t.border != nil {
label := t.executeCommand(a.a, false, true, true, true) label := t.executeCommand(a.a, false, true, true, true)
@@ -3359,6 +3445,7 @@ func (t *Terminal) Loop() {
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actChangePrompt: case actChangePrompt:
t.promptString = a.a
t.prompt, t.promptLen = t.parsePrompt(a.a) t.prompt, t.promptLen = t.parsePrompt(a.a)
req(reqPrompt) req(reqPrompt)
case actPreview: case actPreview:
@@ -3376,7 +3463,7 @@ func (t *Terminal) Loop() {
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
t.delChar() t.delChar()
case actDeleteCharEOF: case actDeleteCharEof:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
@@ -3390,7 +3477,7 @@ func (t *Terminal) Loop() {
t.input = []rune{} t.input = []rune{}
t.cx = 0 t.cx = 0
} }
case actBackwardDeleteCharEOF: case actBackwardDeleteCharEof:
if len(t.input) == 0 { if len(t.input) == 0 {
req(reqQuit) req(reqQuit)
} else if t.cx > 0 { } else if t.cx > 0 {
@@ -3497,6 +3584,12 @@ func (t *Terminal) Loop() {
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 { if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
req(reqClose) req(reqClose)
} }
case actAcceptOrPrintQuery:
if len(t.selected) > 0 || t.merger.Length() > 0 {
req(reqClose)
} else {
req(reqPrintQuery)
}
case actClearScreen: case actClearScreen:
req(reqFullRedraw) req(reqFullRedraw)
case actClearQuery: case actClearQuery:
@@ -3603,7 +3696,7 @@ func (t *Terminal) Loop() {
t.yanked = copySlice(t.input[t.cx:]) t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx] t.input = t.input[:t.cx]
} }
case actRune: case actChar:
prefix := copySlice(t.input[:t.cx]) prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++ t.cx++
@@ -3631,6 +3724,12 @@ func (t *Terminal) Loop() {
t.track = trackEnabled t.track = trackEnabled
} }
req(reqInfo) req(reqInfo)
case actShowHeader:
t.headerVisible = true
req(reqList, reqInfo, reqPrompt, reqHeader)
case actHideHeader:
t.headerVisible = false
req(reqList, reqInfo, reqPrompt, reqHeader)
case actToggleHeader: case actToggleHeader:
t.headerVisible = !t.headerVisible t.headerVisible = !t.headerVisible
req(reqList, reqInfo, reqPrompt, reqHeader) req(reqList, reqInfo, reqPrompt, reqHeader)
@@ -3800,8 +3899,8 @@ func (t *Terminal) Loop() {
// 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
// 2. If the template has {q} // 2. If the template has {q}
slot, _, query := hasPreviewFlags(a.a) slot, _, forceUpdate := hasPreviewFlags(a.a)
valid = !slot || query valid = !slot || forceUpdate
} }
if valid { if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
@@ -3881,6 +3980,10 @@ func (t *Terminal) Loop() {
} }
} }
} }
if !processExecution(a.t) {
t.lastAction = a.t
}
return true return true
} }
@@ -3894,7 +3997,7 @@ func (t *Terminal) Loop() {
actions = t.keymap[event.Comparable()] actions = t.keymap[event.Comparable()]
} }
if len(actions) == 0 && event.Type == tui.Rune { if len(actions) == 0 && event.Type == tui.Rune {
doAction(&action{t: actRune}) doAction(&action{t: actChar})
} else if !doActions(actions) { } else if !doActions(actions) {
continue continue
} }
@@ -3925,8 +4028,8 @@ func (t *Terminal) Loop() {
} }
if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 { if queryChanged && t.canPreview() && len(t.previewOpts.command) > 0 {
_, _, q := hasPreviewFlags(t.previewOpts.command) _, _, forceUpdate := hasPreviewFlags(t.previewOpts.command)
if q { if forceUpdate {
t.version++ t.version++
} }
} }

View File

@@ -12,6 +12,20 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{
template: template,
stripAnsi: stripAnsi,
delimiter: delimiter,
printsep: printsep,
forcePlus: forcePlus,
query: query,
allItems: allItems,
lastAction: actBackwardDeleteCharEof,
prompt: "prompt",
})
}
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1} items1 := []*Item{item1, item1}
@@ -52,90 +66,90 @@ func TestReplacePlaceholder(t *testing.T) {
*/ */
// {}, preserve ansi // {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {}, strip ansi // {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// {}, with multiple items // {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {..}, strip leading whitespaces, preserve ansi // {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
// {..}, strip leading whitespaces, strip ansi // {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
// {q} // {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
// {q}, multiple items // {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2) result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2) result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// forcePlus // forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2) result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}") checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
// Whitespace preserving flag with "'" delimiter // Whitespace preserving flag with "'" delimiter
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.O}}") checkFormat("echo {{.O}} foo{{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}}bar baz{{.O}}") checkFormat("echo {{.O}}bar baz{{.O}}")
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
// Whitespace preserving flag with regex delimiter // Whitespace preserving flag with regex delimiter
regex = regexp.MustCompile(`\w+`) regex = regexp.MustCompile(`\w+`)
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}}{{.I}}{{.O}}") checkFormat("echo {{.O}}{{.I}}{{.O}}")
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
// No match // No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter // String delimiter
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
// Regex delimiter // Regex delimiter
regex = regexp.MustCompile("[oa]+") regex = regexp.MustCompile("[oa]+")
// foo'bar baz // foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1) result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}") checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
/* /*
@@ -155,7 +169,6 @@ func TestReplacePlaceholder(t *testing.T) {
newItem("7a 7b 7c 7d 7e 7f"), newItem("7a 7b 7c 7d 7e 7f"),
} }
stripAnsi := false stripAnsi := false
printsep = "\n"
forcePlus := false forcePlus := false
query := "sample query" query := "sample query"
@@ -198,18 +211,23 @@ func TestReplacePlaceholder(t *testing.T) {
// query flag is not removed after parsing, so it gets doubled // query flag is not removed after parsing, so it gets doubled
// while the double q is invalid, it is useful here for testing purposes // while the double q is invalid, it is useful here for testing purposes
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}" templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
// IV. escaping placeholder // IV. escaping placeholder
templateToOutput[`\{}`] = `{}` templateToOutput[`\{}`] = `{}`
templateToOutput[`\{q}`] = `{q}`
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
templateToOutput[`\{++}`] = `{++}` templateToOutput[`\{++}`] = `{++}`
templateToOutput[`{++}`] = templateToOutput[`{+}`] templateToOutput[`{++}`] = templateToOutput[`{+}`]
for giveTemplate, wantOutput := range templateToOutput { for giveTemplate, wantOutput := range templateToOutput {
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
checkFormat(wantOutput) checkFormat(wantOutput)
} }
for giveTemplate, wantOutput := range templateToFile { for giveTemplate, wantOutput := range templateToFile {
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3) path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
data, err := readFile(path) data, err := readFile(path)
if err != nil { if err != nil {
@@ -563,7 +581,7 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases // evaluate the test cases
for idx, test := range tests { for idx, test := range tests {
gotOutput := replacePlaceholder( gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.give.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query, test.give.query,
test.give.allItems) test.give.allItems)
@@ -605,7 +623,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
if flags.file { if flags.file {
encoded += "f" encoded += "f"
} }
if flags.query { if flags.forceUpdate { // FIXME
encoded += "q" encoded += "q"
} }
return encoded return encoded

View File

@@ -11,6 +11,20 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
var escaper *strings.Replacer
func init() {
tokens := strings.Split(os.Getenv("SHELL"), "/")
if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
}
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH) signal.Notify(resizeChan, syscall.SIGWINCH)
} }
@@ -29,5 +43,5 @@ func notifyOnCont(resizeChan chan<- os.Signal) {
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + escaper.Replace(entry) + "'"
} }

View File

@@ -741,6 +741,12 @@ class TestGoFZF < TestBase
'xxoxxxxxxx', 'xxoxxxxxxx',
'xoxxxxxxxx' 'xoxxxxxxxx'
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true) ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
writelines(tempname, ['/bar/baz', '/foo/bar/baz'])
assert_equal [
'/foo/bar/baz',
'/bar/baz'
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
end end
def test_tiebreak_length_with_nth def test_tiebreak_length_with_nth
@@ -1776,6 +1782,35 @@ class TestGoFZF < TestBase
assert_equal %w[foo], readonce.lines(chomp: true) assert_equal %w[foo], readonce.lines(chomp: true)
end end
def test_accept_or_print_query_without_match
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[99999], readonce.lines(chomp: true)
end
def test_accept_or_print_query_with_match
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys '^99$'
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[99], readonce.lines(chomp: true)
end
def test_accept_or_print_query_with_multi_selection
tmux.send_keys %(seq 1000 | #{fzf('--bind enter:accept-or-print-query --multi')}), :Enter
tmux.until { |lines| assert_equal 1000, lines.match_count }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_equal 3, lines.select_count }
tmux.send_keys 99_999
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
assert_equal %w[1 2 3], readonce.lines(chomp: true)
end
def test_preview_update_on_select def test_preview_update_on_select
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
:Enter :Enter
@@ -1987,6 +2022,13 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '> RAB', lines[-1] } tmux.until { |lines| assert_equal '> RAB', lines[-1] }
end end
def test_transform
tmux.send_keys %{#{FZF} --bind 'focus:transform:echo "change-prompt({fzf:action})"'}, :Enter
tmux.until { |lines| assert_equal 'start', lines[-1] }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 'up', lines[-1] }
end
def test_clear_selection def test_clear_selection
tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter tmux.send_keys %(seq 100 | #{FZF} --multi --bind space:clear-selection), :Enter
tmux.until { |lines| assert_equal 100, lines.match_count } tmux.until { |lines| assert_equal 100, lines.match_count }