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

Compare commits

...

17 Commits

Author SHA1 Message Date
Junegunn Choi
2093667548 0.30.0 2022-04-04 23:01:43 +09:00
Junegunn Choi
3c868d7961 ADVANCED.md: Add rebind example 2022-04-04 22:59:26 +09:00
dependabot[bot]
707f4f5816 Bump github/codeql-action from 1.1.5 to 2.1.6 (#2782)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1.1.5 to 2.1.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8834766498...28eead2408)

---
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>
2022-04-04 22:44:10 +09:00
Junegunn Choi
b3ab6311c5 Hide cursor while rendering the screen
Fix #2781
Fix #2588
Fix #1805

Fix https://github.com/junegunn/fzf.vim/issues/1370
Fix https://github.com/junegunn/fzf.vim/issues/1060
2022-04-04 22:06:16 +09:00
Junegunn Choi
d56f605b63 Add rebind action for restoring bindings after unbind
Fix #2752
Close #2564
2022-04-04 21:54:22 +09:00
Junegunn Choi
f8b713f425 Remove redundant state update on reload
Related: 5209e95
2022-03-31 10:05:28 +09:00
Junegunn Choi
5209e95bc7 Make preview updated when reload and change-query are combined
Fix #2744
2022-03-29 22:27:03 +09:00
Junegunn Choi
ef67a45702 Add --ellipsis=.. option
Close #2432

Also see
- #1769
- https://github.com/junegunn/fzf/pull/1844#issuecomment-586663660
2022-03-29 21:35:36 +09:00
Junegunn Choi
b88eb72ac2 Modernize build tags 2022-03-29 21:23:45 +09:00
dependabot[bot]
32847f7254 Bump actions/setup-go from 2.2.0 to 3 (#2776)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](bfdd3570ce...f6164bd8c8)

---
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>
2022-03-29 21:17:44 +09:00
dependabot[bot]
71df93b534 Bump ruby/setup-ruby from 1.62.0 to 1.100.0 (#2775)
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.62.0 to 1.100.0.
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Commits](5aaa89ff0d...bd94d6a504)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  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>
2022-03-29 21:17:29 +09:00
Naveen
bb028191f8 Set up dependabot for GitHub actions (#2764) 2022-03-29 21:08:41 +09:00
Naveen
19af8fc7d8 Pin actions to a full length commit SHA (#2765)
- Pinned actions by SHA https://github.com/ossf/scorecard/blob/main/docs/checks.md#pinned-dependencies
- Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

>Pin actions to a full length commit SHA

>Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload.

https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions

Also, dependabot supports upgrade based on SHA.

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>s
2022-03-29 21:08:01 +09:00
Junegunn Choi
a06671b47f Increase TTY buffer limit
Kitty's shell intergration generates a long sequence of key presses in
certain cases. As long as the length of the sequence is finite, fzf can
process it.

Close #2748
2022-03-09 17:02:06 +09:00
Junegunn Choi
5f385d88e0 [zsh] Set up bindings for all three keymaps: emacs, vicmd, and viins
Fix #2694
2022-02-23 15:36:49 +09:00
Junegunn Choi
9cb7a364a3 [install] Remove code that might delete user fish script
Fix #2703
2022-01-05 21:48:20 +09:00
Junegunn Choi
f68cbc577d Add link to ADVANCED.md
Related #2701
2022-01-03 13:52:46 +09:00
37 changed files with 245 additions and 85 deletions

View File

@@ -4,3 +4,7 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -8,8 +8,15 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
analyze: analyze:
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -20,18 +27,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@28eead240834b314f7def40f6fcba65d100d99b1 # v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@28eead240834b314f7def40f6fcba65d100d99b1 # v1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@28eead240834b314f7def40f6fcba65d100d99b1 # v1

View File

@@ -8,24 +8,24 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
go: [1.14, 1.16]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: ${{ matrix.go }} go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0 uses: ruby/setup-ruby@bd94d6a504586da892a5753afdd1480096ed30df # v1.62.0
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

View File

@@ -8,24 +8,24 @@ on:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
strategy:
matrix:
go: [1.14, 1.16]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with: with:
go-version: ${{ matrix.go }} go-version: 1.18
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0 uses: ruby/setup-ruby@bd94d6a504586da892a5753afdd1480096ed30df # v1.62.0
with: with:
ruby-version: 3.0.0 ruby-version: 3.0.0

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.18

View File

@@ -17,6 +17,7 @@ Advanced fzf examples
* [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 interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher) * [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode) * [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
* [Log tailing](#log-tailing) * [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)
@@ -405,6 +406,40 @@ IFS=: read -ra selected < <(
- We reverted `--color` option for customizing how the matching chunks are - We reverted `--color` option for customizing how the matching chunks are
displayed in the second phase displayed in the second phase
### Switching between Ripgrep mode and fzf mode
*(Requires fzf 0.30.0 or above)*
fzf 0.30.0 added `rebind` action so we can "rebind" the bindings that were
previously "unbound" via `unbind`.
This is an improved version of the previous example that allows us to switch
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
CTRL-F.
```sh
#!/usr/bin/env bash
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY="${*:-}"
IFS=: read -ra selected < <(
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
fzf --ansi \
--color "hl:-1:underline,hl+:-1:underline:reverse" \
--disabled --query "$INITIAL_QUERY" \
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" \
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)" \
--prompt '1. Ripgrep> ' \
--delimiter : \
--header ' CTRL-R (Ripgrep mode) CTRL-F (fzf mode) ' \
--preview 'bat --color=always {1} --highlight-line {2}' \
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
)
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
```
Log tailing Log tailing
----------- -----------

View File

@@ -1,6 +1,21 @@
CHANGELOG CHANGELOG
========= =========
0.30.0
------
- Fixed cursor flickering over the screen by hiding it during rendering
- Added `--ellipsis` option. You can take advantage of it to make fzf
effectively search non-visible parts of the item.
```sh
# Search against hidden line numbers on the far right
nl /usr/share/dict/words |
awk '{printf "%s%1000s\n", $2, $1}' |
fzf --nth=-1 --no-hscroll --ellipsis='' |
awk '{print $2}'
```
- Added `rebind` action for restoring bindings after `unbind`
- Bug fixes and improvements
0.29.0 0.29.0
------ ------
- Added `change-preview(...)` action to change the `--preview` command - Added `change-preview(...)` action to change the `--preview` command

View File

@@ -592,6 +592,9 @@ If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
and fzf will warn you about it. To suppress the warning message, we added and fzf will warn you about it. To suppress the warning message, we added
`|| true` to the command, so that it always exits with 0. `|| true` to the command, so that it always exits with 0.
See ["Using fzf as interative Ripgrep launcher"](https://github.com/junegunn/fzf/blob/master/ADVANCED.md#using-fzf-as-interative-ripgrep-launcher)
for a fuller example with preview window options.
### Preview window ### Preview window
When the `--preview` option is set, fzf automatically starts an external process When the `--preview` option is set, fzf automatically starts an external process

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.29.0 version=0.30.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -280,11 +280,6 @@ EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p "${fish_dir}/functions" mkdir -p "${fish_dir}/functions"
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
fi
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish" fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
if [ $key_bindings -ne 0 ]; then if [ $key_bindings -ne 0 ]; then
echo -n "Symlink $fish_binding ... " echo -n "Symlink $fish_binding ... "

View File

@@ -1,4 +1,4 @@
$version="0.29.0" $version="0.30.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.29" var version string = "0.30"
var revision string = "devel" var revision string = "devel"
func main() { func main() {

View File

@@ -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 "Dec 2021" "fzf 0.29.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Apr 2022" "fzf 0.30.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

@@ -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 "Dec 2021" "fzf 0.29.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2022" "fzf 0.30.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -302,6 +302,9 @@ lines that follow.
.TP .TP
.B "--header-first" .B "--header-first"
Print header before the prompt line Print header before the prompt line
.TP
.BI "--ellipsis=" "STR"
Ellipsis to show when line is truncated (default: '..')
.SS Display .SS Display
.TP .TP
.B "--ansi" .B "--ansi"
@@ -864,6 +867,7 @@ A key or an event can be bound to one or more of the following actions.
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
\fBrebind(...)\fR (rebind bindings after \fBunbind\fR)
\fBreload(...)\fR (see below for the details) \fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection) \fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR \fBselect\fR

View File

@@ -65,8 +65,10 @@ fzf-file-widget() {
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey -M emacs '^T' fzf-file-widget
bindkey -M vicmd '^T' fzf-file-widget
bindkey -M viins '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
@@ -86,8 +88,10 @@ fzf-cd-widget() {
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
bindkey '\ec' fzf-cd-widget bindkey -M emacs '\ec' fzf-cd-widget
bindkey -M vicmd '\ec' fzf-cd-widget
bindkey -M viins '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
@@ -105,8 +109,10 @@ fzf-history-widget() {
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
} always { } always {
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options

View File

@@ -242,9 +242,11 @@ func Run(opts *Options, version string, revision string) {
for { for {
delay := true delay := true
ticks++ ticks++
input := func() []rune { input := func(reloaded bool) []rune {
paused, input := terminal.Input() paused, input := terminal.Input()
if !paused { if reloaded && paused {
query = []rune{}
} else if !paused {
query = input query = input
} }
return query return query
@@ -274,7 +276,8 @@ func Run(opts *Options, version string, revision string) {
opts.Sync = false opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
} }
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache()) reset := clearCache()
matcher.Reset(snapshot, input(reset), false, !reading, sort, reset)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *string
@@ -293,7 +296,8 @@ func Run(opts *Options, version string, revision string) {
break break
} }
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache()) reset := clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:

View File

@@ -70,6 +70,7 @@ const usage = `usage: fzf [options]
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
--header-first Print header before the prompt line --header-first Print header before the prompt line
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
Display Display
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
@@ -235,6 +236,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Ellipsis string
Margin [4]sizeSpec Margin [4]sizeSpec
Padding [4]sizeSpec Padding [4]sizeSpec
BorderShape tui.BorderShape BorderShape tui.BorderShape
@@ -298,6 +300,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Ellipsis: "..",
Margin: defaultMargin(), Margin: defaultMargin(),
Padding: defaultMargin(), Padding: defaultMargin(),
Unicode: true, Unicode: true,
@@ -795,7 +798,7 @@ func init() {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[tui.Event][]*action, str string) { func parseKeymap(keymap map[tui.Event][]*action, str string) {
@@ -815,6 +818,8 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
prefix = symbol + "preview" prefix = symbol + "preview"
} else if strings.HasPrefix(src[1:], "unbind") { } else if strings.HasPrefix(src[1:], "unbind") {
prefix = symbol + "unbind" prefix = symbol + "unbind"
} else if strings.HasPrefix(src[1:], "rebind") {
prefix = symbol + "rebind"
} else if strings.HasPrefix(src[1:], "change-prompt") { } else if strings.HasPrefix(src[1:], "change-prompt") {
prefix = symbol + "change-prompt" prefix = symbol + "change-prompt"
} else if src[len(prefix)] == '-' { } else if src[len(prefix)] == '-' {
@@ -1022,6 +1027,8 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
offset = len("change-prompt") offset = len("change-prompt")
case actUnbind: case actUnbind:
offset = len("unbind") offset = len("unbind")
case actRebind:
offset = len("rebind")
case actExecuteSilent: case actExecuteSilent:
offset = len("execute-silent") offset = len("execute-silent")
case actExecuteMulti: case actExecuteMulti:
@@ -1042,8 +1049,8 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
actionArg = spec[offset+1 : len(spec)-1] actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
if t == actUnbind { if t == actUnbind || t == actRebind {
parseKeyChords(actionArg, "unbind target required") parseKeyChords(actionArg, spec[0:offset]+" target required")
} else if t == actChangePreviewWindow { } else if t == actChangePreviewWindow {
opts := previewOpts{} opts := previewOpts{}
for _, arg := range strings.Split(actionArg, "|") { for _, arg := range strings.Split(actionArg, "|") {
@@ -1072,6 +1079,8 @@ func isExecuteAction(str string) actionType {
return actReload return actReload
case "unbind": case "unbind":
return actUnbind return actUnbind
case "rebind":
return actRebind
case "preview": case "preview":
return actPreview return actPreview
case "change-preview-window": case "change-preview-window":
@@ -1280,6 +1289,7 @@ func parseOptions(opts *Options, allArgs []string) {
validateJumpLabels := false validateJumpLabels := false
validatePointer := false validatePointer := false
validateMarker := false validateMarker := false
validateEllipsis := false
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -1465,6 +1475,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.HeaderFirst = true opts.HeaderFirst = true
case "--no-header-first": case "--no-header-first":
opts.HeaderFirst = false opts.HeaderFirst = false
case "--ellipsis":
opts.Ellipsis = nextString(allArgs, &i, "ellipsis string required")
validateEllipsis = true
case "--preview": case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required") opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview": case "--no-preview":
@@ -1562,6 +1575,9 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Header = strLines(value) opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--ellipsis="); match {
opts.Ellipsis = value
validateEllipsis = true
} else if match, value := optString(arg, "--preview="); match { } else if match, value := optString(arg, "--preview="); match {
opts.Preview.command = value opts.Preview.command = value
} else if match, value := optString(arg, "--preview-window="); match { } else if match, value := optString(arg, "--preview-window="); match {
@@ -1624,6 +1640,14 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit(err.Error()) errorExit(err.Error())
} }
} }
if validateEllipsis {
for _, r := range opts.Ellipsis {
if !unicode.IsGraphic(r) {
errorExit("invalid character in ellipsis")
}
}
}
} }
func validateSign(sign string, signOptName string) error { func validateSign(sign string, signOptName string) error {

View File

@@ -1,4 +1,4 @@
// +build !openbsd //go:build !openbsd
package protector package protector

View File

@@ -1,4 +1,4 @@
// +build openbsd //go:build openbsd
package protector package protector

View File

@@ -1,4 +1,4 @@
// +build !386,!amd64 //go:build !386 && !amd64
package fzf package fzf

View File

@@ -1,5 +1,3 @@
// +build !tcell
package fzf package fzf
import ( import (

View File

@@ -1,4 +1,4 @@
// +build 386 amd64 //go:build 386 || amd64
package fzf package fzf

View File

@@ -50,7 +50,6 @@ var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string var activeTempFiles []string
const ellipsis string = ".."
const clearCode string = "\x1b[2J" const clearCode string = "\x1b[2J"
func init() { func init() {
@@ -137,6 +136,7 @@ type Terminal struct {
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]*action keymap map[tui.Event][]*action
keymapOrg map[tui.Event][]*action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@@ -145,6 +145,7 @@ type Terminal struct {
headerLines int headerLines int
header []string header []string
header0 []string header0 []string
ellipsis string
ansi bool ansi bool
tabstop int tabstop int
margin [4]sizeSpec margin [4]sizeSpec
@@ -313,6 +314,7 @@ const (
actSelect actSelect
actDeselect actDeselect
actUnbind actUnbind
actRebind
) )
type placeholderFlags struct { type placeholderFlags struct {
@@ -501,6 +503,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
} }
keymapCopy := make(map[tui.Event][]*action)
for key, action := range opts.Keymap {
keymapCopy[key] = action
}
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
@@ -526,6 +532,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
keymapOrg: keymapCopy,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
@@ -541,6 +548,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: header,
header0: header, header0: header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
@@ -672,6 +680,7 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
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.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
@@ -1261,47 +1270,54 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current) offsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1) maxWidth := t.window.Width() - (t.pointerLen + t.markerLen + 1)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(text))
displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
if displayWidth > maxWidth { if displayWidth > maxWidth {
transformOffsets := func(diff int32) { transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offsets { for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1] b, e := offset.offset[0], offset.offset[1]
b += 2 - diff el := int32(len(ellipsis))
e += 2 - diff b += el - diff
b = util.Max32(b, 2) e += el - diff
b = util.Max32(b, el)
if rightTrim {
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offsets[idx].offset[0] = b offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e) offsets[idx].offset[1] = util.Max32(b, e)
} }
} }
if t.hscroll { if t.hscroll {
if t.keepRight && pos == nil { if t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(text, maxWidth-2) trimmed, diff := t.trimLeft(text, maxWidth-ellipsisWidth)
transformOffsets(diff) transformOffsets(diff, false)
text = append([]rune(ellipsis), trimmed...) text = append(ellipsis, trimmed...)
} else if !t.overflow(text[:maxe], maxWidth-2) { } else if !t.overflow(text[:maxe], maxWidth-ellipsisWidth) {
// Stri.. // Stri..
text, _ = t.trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, []rune(ellipsis)...) text = append(text, ellipsis...)
} else { } else {
// Stri.. // Stri..
if t.overflow(text[maxe:], 2) { rightTrim := false
text = append(text[:maxe], []rune(ellipsis)...) if t.overflow(text[maxe:], ellipsisWidth) {
text = append(text[:maxe], ellipsis...)
rightTrim = true
} }
// ..ri.. // ..ri..
var diff int32 var diff int32
text, diff = t.trimLeft(text, maxWidth-2) text, diff = t.trimLeft(text, maxWidth-ellipsisWidth)
// Transform offsets // Transform offsets
transformOffsets(diff) transformOffsets(diff, rightTrim)
text = append([]rune(ellipsis), text...) text = append(ellipsis, text...)
} }
} else { } else {
text, _ = t.trimRight(text, maxWidth-2) text, _ = t.trimRight(text, maxWidth-ellipsisWidth)
text = append(text, []rune(ellipsis)...) text = append(text, ellipsis...)
for idx, offset := range offsets { for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2)) offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} }
@@ -2719,13 +2735,19 @@ func (t *Terminal) Loop() {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
newCommand = &command newCommand = &command
t.reading = true t.reading = true
t.version++
} }
case actUnbind: case actUnbind:
keys := parseKeyChords(a.a, "PANIC") keys := parseKeyChords(a.a, "PANIC")
for key := range keys { for key := range keys {
delete(t.keymap, key) delete(t.keymap, key)
} }
case actRebind:
keys := parseKeyChords(a.a, "PANIC")
for key := range keys {
if originalAction, found := t.keymapOrg[key]; found {
t.keymap[key] = originalAction
}
}
case actChangePreview: case actChangePreview:
if t.previewOpts.command != a.a { if t.previewOpts.command != a.a {
togglePreview(len(a.a) > 0) togglePreview(len(a.a) > 0)

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package fzf package fzf

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package fzf package fzf

View File

@@ -1,6 +1,4 @@
// +build !ncurses //go:build !tcell && !windows
// +build !tcell
// +build !windows
package tui package tui

View File

@@ -23,7 +23,7 @@ const (
defaultEscDelay = 100 defaultEscDelay = 100
escPollInterval = 5 escPollInterval = 5
offsetPollTries = 10 offsetPollTries = 10
maxInputBuffer = 10 * 1024 maxInputBuffer = 1024 * 1024
) )
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
@@ -60,7 +60,7 @@ func (r *LightRenderer) csi(code string) {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, r.queued.String()) fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
r.queued.Reset() r.queued.Reset()
} }
} }

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package tui package tui

View File

@@ -1,4 +1,4 @@
//+build windows //go:build windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build tcell windows //go:build tcell || windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build tcell windows //go:build tcell || windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package tui package tui

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package tui package tui

View File

@@ -34,6 +34,23 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int
return width, -1 return width, -1
} }
// Truncate returns the truncated runes and its width
func Truncate(input string, limit int) ([]rune, int) {
runes := []rune{}
width := 0
gr := uniseg.NewGraphemes(input)
for gr.Next() {
rs := gr.Runes()
w := runewidth.StringWidth(string(rs))
if width+w > limit {
return runes, width
}
width += w
runes = append(runes, rs...)
}
return runes, width
}
// Max returns the largest integer // Max returns the largest integer
func Max(first int, second int) int { func Max(first int, second int) int {
if first >= second { if first >= second {

View File

@@ -54,3 +54,13 @@ func TestRunesWidth(t *testing.T) {
} }
} }
} }
func TestTruncate(t *testing.T) {
truncated, width := Truncate("가나다라마", 7)
if string(truncated) != "가나다" {
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
}
if width != 6 {
t.Errorf("Expected: 6, actual: %d", width)
}
}

View File

@@ -1,4 +1,4 @@
// +build !windows //go:build !windows
package util package util

View File

@@ -1,4 +1,4 @@
// +build windows //go:build windows
package util package util

View File

@@ -2043,8 +2043,8 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) } tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
end end
def test_unbind def test_unbind_rebind
tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d)'", :Enter tmux.send_keys "seq 100 | #{FZF} --bind 'c:clear-query,d:unbind(c,d),e:rebind(c,d)'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'ab' tmux.send_keys 'ab'
tmux.until { |lines| assert_equal '> ab', lines[-1] } tmux.until { |lines| assert_equal '> ab', lines[-1] }
@@ -2052,6 +2052,8 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines[-1] } tmux.until { |lines| assert_equal '>', lines[-1] }
tmux.send_keys 'dabcd' tmux.send_keys 'dabcd'
tmux.until { |lines| assert_equal '> abcd', lines[-1] } tmux.until { |lines| assert_equal '> abcd', lines[-1] }
tmux.send_keys 'ecabddc'
tmux.until { |lines| assert_equal '> abdc', lines[-1] }
end end
def test_item_index_reset_on_reload def test_item_index_reset_on_reload
@@ -2077,6 +2079,15 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_includes lines[1], '4' } tmux.until { |lines| assert_includes lines[1], '4' }
end end
def test_reload_and_change_preview_should_update_preview
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.until { |lines| refute_includes lines[1], '1' }
tmux.send_keys 'C-t'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.until { |lines| assert_includes lines[1], '4' }
end
def test_scroll_off def test_scroll_off
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
tmux.until { |lines| assert_equal 1000, lines.item_count } tmux.until { |lines| assert_equal 1000, lines.item_count }
@@ -2208,6 +2219,12 @@ class TestGoFZF < TestBase
tmux.send_keys 'a' tmux.send_keys 'a'
end end
end end
def test_ellipsis
tmux.send_keys 'seq 1000 | tr "\n" , | fzf --ellipsis=SNIPSNIP -e -q500', :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.until { |lines| assert_match(/^> SNIPSNIP.*SNIPSNIP$/, lines[-3]) }
end
end end
module TestShell module TestShell