Compare commits

..

137 Commits

Author SHA1 Message Date
Junegunn Choi c4a9ccd6af 0.53.0 2024-06-06 22:03:26 +09:00
Junegunn Choi cbf91f2ed3 ADVANCED.md: /dev/tty redirection no longer required 2024-06-06 21:58:02 +09:00
Junegunn Choi b1460d4787 hasPreviewFlags should ignore escaped placeholder
This reload command wouldn't run before the fix:

  : | fzf --bind 'start:reload:echo \{}'
2024-06-06 17:40:15 +09:00
Junegunn Choi 7dc9e14874 Update docs 2024-06-06 17:40:15 +09:00
Junegunn Choi 1616ed543d Fix index out of bounds error caused by outdated offset 2024-06-06 00:23:58 +09:00
Junegunn Choi dc73fba188 [man] Clarification on --scheme options 2024-06-05 14:29:50 +09:00
Junegunn Choi ef148dfd37 Handle int32 overflow
yes | fzf --tail=10 --preview 'echo "{n}"'
2024-06-05 14:29:50 +09:00
Junegunn Choi 93bbb3032d Add --tail=NUM to limit the number of items to keep in memory 2024-06-04 17:50:46 +09:00
Junegunn Choi 4c83d8596d Add new options to bash completion 2024-06-03 09:45:20 +09:00
Junegunn Choi d453e6d7db Update ADVANCED.md: Use --tmux instead of fzf-tmux 2024-06-03 09:41:40 +09:00
Junegunn Choi c29533994f Fix invalid default of selected-hl (--color)
It should default to 'hl' instead of 'current-hl'
2024-06-02 18:09:41 +09:00
Junegunn Choi 1afe13b5b5 Merge remote-tracking branch 'origin/master' into devel 2024-06-02 17:59:04 +09:00
Junegunn Choi 36600eaaa9 Update CHANGELOG: clarification 2024-06-02 17:58:44 +09:00
junegunn 3ee1fc2034 Deploying to master from @ junegunn/fzf@124cd70710 🚀 2024-06-02 00:01:52 +00:00
Junegunn Choi e2f93e5a2d --tmux vs. --height: Last one wins 2024-06-01 22:11:15 +09:00
Junegunn Choi cfdf2f1153 Update README 2024-06-01 16:20:03 +09:00
Junegunn Choi e042143e3f Immediately close standard output of the child process
Fix #3828
2024-06-01 15:22:05 +09:00
Junegunn Choi 7c613d0d9b Do not disable --height on mintty (because it works) 2024-06-01 14:45:54 +09:00
Junegunn Choi b00d46bc14 Fix --height on Windows 2024-06-01 14:36:41 +09:00
Junegunn Choi 555b0d235b Ignore --height option if it's not supported on the platform
This is to make shell integration work out of the box on Git bash.

  eval "$(fzf --bash)"
  vim <CTRL-T>
    # would print '--height option is currently not supported on this platform'
2024-06-01 14:35:45 +09:00
Junegunn Choi 564daf9a7d Set standard input of 'man' process to os.Stdin 2024-06-01 13:23:46 +09:00
Junegunn Choi 41bcbe342f Revert "An '--expect' key should execute actions bound to the key"
To be backward compatible.

Close #3829
2024-06-01 13:21:59 +09:00
LangLangBart dbe8dc344e [fish] Use builtins for cd and history (#3830)
Close #3826
2024-06-01 11:28:02 +09:00
Junegunn Choi e33fb59da1 Update CHANGELOG 2024-05-31 16:57:35 +09:00
Junegunn Choi 7aa88aa115 Fix error message on invalid --tmux option
fzf --tmux foobar
  # not a valid integer: foobar
  # ->
  # invalid tmux option: foobar (expected: [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]])
2024-05-31 16:57:35 +09:00
LangLangBart 2b6d600879 [zsh] Enhance CTRL-R to display multi-line entires (#3823)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-31 16:57:35 +09:00
Junegunn Choi 05c765d442 [fish] Add --nth 2..,.. to allow anchored search against command 2024-05-31 16:57:35 +09:00
Junegunn Choi 49b496269c Fix index out of bounds error on scroll-down action 2024-05-31 16:57:35 +09:00
Junegunn Choi 7405925952 [bash] Indent multi-line history entries 2024-05-31 16:57:35 +09:00
Junegunn Choi 3afd543a7e [fish] Use perl instead of sed to strip leading tabs
https://github.com/junegunn/fzf/pull/3807#discussion_r1619520105
2024-05-30 10:23:20 +09:00
Junegunn Choi b4f2cde5ac [fish] Better multi-line support for CTRL-R
Prepend each entry with an index number so that multi-line entries can
be clearly distinguished.
2024-05-29 20:16:49 +09:00
Junegunn Choi ed53ef7cee [shell] Add --highlight-line to CTRL-R bindings 2024-05-29 20:13:41 +09:00
Junegunn Choi 12630b124d Make --tmux argument optional 2024-05-29 02:16:18 +09:00
Junegunn Choi 1d59ac09d2 Pass-through error message from 'tmux display-popup'
fzf --tmux 9999
    # height too large
2024-05-29 02:07:56 +09:00
Junegunn Choi a8f3a0dd59 Merge branch 'master' into devel 2024-05-28 23:19:26 +09:00
Konstantin-Glukhov 124cd70710 [vim] Do not prepend CWD to path starting with a backslash on Windows (#3820)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-28 23:15:14 +09:00
Junegunn Choi 782de139c8 [vim] Native --tmux fix for Neovim 2024-05-28 19:27:31 +09:00
Junegunn Choi 32eb32ee5e Add multi-line example to CHANGELOG 2024-05-27 01:48:46 +09:00
Junegunn Choi 2f51eb2b41 Different marker for the first and last line of multi-line entries
Can be configured via `--marker-multi-line`
2024-05-27 01:35:05 +09:00
Junegunn Choi 0ccbd79e10 Fix --help output: marker default
Co-authored-by: LangLangBart <92653266+LangLangBart@users.noreply.github.com>
2024-05-26 09:24:30 +09:00
junegunn 99bd6de541 Deploying to master from @ junegunn/fzf@daa602422d 🚀 2024-05-26 00:01:51 +00:00
Junegunn Choi 1fef36e4bc Do not allow tabs in pointer and marker 2024-05-25 16:31:34 +09:00
Junegunn Choi 89375005b5 Fix option validation order 2024-05-25 16:23:13 +09:00
Junegunn Choi 88e78c9193 Update integration test to use named pipes 2024-05-25 12:03:20 +09:00
Junegunn Choi 29a19ad080 Update CHANGELOG 2024-05-25 09:40:17 +09:00
Junegunn Choi 2a039ab746 Describe exit code 126 2024-05-24 19:32:44 +09:00
Junegunn Choi 7e9a0fcdbd Change default --scroll-off to 3 2024-05-24 19:25:50 +09:00
Junegunn Choi 7a97532547 Fix --scroll-off for multi-line mode 2024-05-24 19:23:36 +09:00
Junegunn Choi 996abb2831 Fix incorrect colors for selected-{fg,bg,hl}
When a non-default base color scheme is specified, fzf would choose incorrect
colors for selected-*.

  fzf --color 'light,fg:238,bg:255,bg+:253' -m
2024-05-24 00:46:01 +09:00
Junegunn Choi da500a358f Use bold bar as the default marker 2024-05-24 00:31:20 +09:00
Junegunn Choi c36b846acc [vim] Open cmd.exe window only on mintty < 3.4.5 without winpty 2024-05-23 21:27:29 +09:00
Junegunn Choi d9b5c9b2be Address review comments by @Konfekt
https://github.com/junegunn/fzf/commit/d4216b0dcc13567479d81cc5ad2adedb1443ea8b
2024-05-23 21:14:08 +09:00
Junegunn Choi 3dee8778d0 execute: Open separate handles to /dev/tty (in, out, err)
# This will no longer cause 'Vim: Warning: Output is not to a terminal'
  fzf --bind 'enter:execute:vim {}' > /tmp/foo
2024-05-23 21:11:12 +09:00
Junegunn Choi d4216b0dcc Use MSYS=enable_pcon instead of winpty on mintty 3.4.5 or later 2024-05-23 18:42:54 +09:00
Enno bfe2bf4dce [vim] Git Bash Mintty: only use cmd.exe if winpty missing (#3811)
* Git Bash Mintty: only use cmd.exe if winpty missing

Addresses https://github.com/junegunn/fzf/issues/3809

* preferably use term in Git Bash for popup window

See https://github.com/junegunn/fzf/pull/3811#issuecomment-2124241321
2024-05-23 09:07:54 +09:00
Junegunn Choi 561f9291fd [vim] Replace backslashes with forward slashes on win32unix 2024-05-23 09:03:43 +09:00
Junegunn Choi b5b0d6b3ea Do not run as winpty proxy if winpty is not available 2024-05-23 08:47:38 +09:00
Junegunn Choi a90426b7ca Add print(...) action 2024-05-22 22:18:24 +09:00
Junegunn Choi 303c3bae7f proxy: Pass SIGINT to the child fzf 2024-05-22 22:14:00 +09:00
Junegunn Choi 6b4358f641 An '--expect' key should execute actions bound to the key
Fix #3810
2024-05-22 20:39:09 +09:00
Junegunn Choi 552158f3ad Ignore SIGINT when running as proxy 2024-05-22 20:01:37 +09:00
Junegunn Choi 7205203dc8 Update CHANGELOG 2024-05-21 02:07:49 +09:00
Junegunn Choi 0cadf70072 Update the summary 2024-05-21 01:57:22 +09:00
Junegunn Choi 076b3d0a9a Embed man page in the binary and show it on 'fzf --man' 2024-05-21 01:06:10 +09:00
Junegunn Choi 7b0c9e04d3 Change default marker 2024-05-20 18:51:52 +09:00
Junegunn Choi 573df524fe Use winpty to launch fzf in Git bash (mintty)
Close #3806

Known limitation:
* --height cannot be used
2024-05-20 18:24:14 +09:00
Junegunn Choi aee417c46a Respect $NO_COLOR environment variable
Close #1762
2024-05-20 10:50:00 +09:00
Junegunn Choi 04db44067d Implement multi-line display of multi-line items 2024-05-20 09:25:30 +09:00
Junegunn Choi 5b204c54f9 Change default pointer and marker character
* Pointer: '▌'
* Marker: '▏'

They will still be set to '>' if `--no-unicode` is given.

Reasons:
* They look okay
* They work better with multi-line items (WIP)
2024-05-19 15:51:32 +09:00
junegunn daa602422d Deploying to master from @ junegunn/fzf@01e7668915 🚀 2024-05-19 00:01:47 +00:00
Junegunn Choi 04dfb14e32 Do not 'become' inside a tmux popup
fzf --tmux center --bind 'enter:become:vim {}'
2024-05-18 17:08:36 +09:00
Junegunn Choi c24256cba3 Update README
* Tidy up
* Mention `--tmux`
2024-05-18 17:08:36 +09:00
Junegunn Choi 685fb71d89 [vim] Use native --tmux option instead of fzf-tmux when possible 2024-05-18 17:08:36 +09:00
Junegunn Choi 83b6033906 Add --tmux option to replace fzf-tmux script 2024-05-18 17:08:36 +09:00
Zhizhen He 01e7668915 chore: use strings.ReplaceAll (#3801) 2024-05-18 17:06:33 +09:00
Enno 0994d9c881 Make :FZF work in Vim from Git Bash (#3798)
* make :FZF work in Vim from Git Bash

Despite its title 'Calling fzf#run with a list as source fail (n)vim is used from git bash' the issue in 

https://github.com/junegunn/fzf/issues/3777

of running `:FZF` in Vim in Git Bash was apparently only fixed for Neovim in Git Bash on Windows 11, but not for Vim from Git Bash.

In view of this, replacing /C by ///C might be considered a universal fix.

This PR just proposes the patch in https://github.com/junegunn/fzf/issues/1983 that still seems open.

In view of the fourth item in the most recent 2.45.0 https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues little seems to have changed regarding path conversion of arguments containing forward slashes

* prefer doubling slashed instead of generic env. var

If MSYS_NO_PATHCONV=1 is used, then all arguments are preserved, in particular possibly paths passed in s:command.
Therefore, only avoid converting `/C` from `cmd` to a path.
2024-05-15 17:26:49 +09:00
LangLangBart 030428ba43 docs: update zsh integration instructions (#3794) 2024-05-15 01:59:43 +09:00
Junegunn Choi 8a110e02b9 Fix tcell test case 2024-05-15 00:45:23 +09:00
Junegunn Choi 86d92c17c4 Refactor tui.TtyIn() 2024-05-15 00:28:56 +09:00
Junegunn Choi c4cc7891b4 Revert "Close handles to /dev/tty", instead reuse handles 2024-05-15 00:15:29 +09:00
Junegunn Choi 218843b9f1 Close handles to /dev/tty 2024-05-14 21:49:47 +09:00
Junegunn Choi d274d093af Render UI directly to /dev/tty
See https://github.com/junegunn/fzf/discussions/3792

This allows us to separately capture the standard error from fzf and its
child processes, and there's less chance of user errors of redirecting
the error stream and hiding fzf.
2024-05-14 16:32:26 +09:00
Junegunn Choi 6432f00f0d 0.52.1 2024-05-14 01:54:30 +09:00
junegunn 4e9e842aa4 Deploying to master from @ junegunn/fzf@07880ca441 🚀 2024-05-12 00:01:52 +00:00
LangLangBart 07880ca441 chore: Update flags to include long-form options for case (#3785) 2024-05-09 20:39:21 +09:00
Junegunn Choi bcda25a513 0.52.0 2024-05-08 00:15:30 +09:00
Junegunn Choi 8256fcde15 Minor fixup 2024-05-07 23:49:30 +09:00
Junegunn Choi af65aa298a Add color names: selected-{fg,bg,hl} 2024-05-07 23:38:06 +09:00
Junegunn Choi 6834d17844 [vim] Revert 7411da8d5a
Fix #3777
2024-05-07 20:16:18 +09:00
Junegunn Choi ed511d7867 [install] tar --no-same-owner
Close #3776
2024-05-07 20:00:13 +09:00
Junegunn Choi cd8d736a9f [shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2024-05-07 19:31:56 +09:00
Junegunn Choi 0952b2dfd4 Rename --cursor-line to --highlight-line 2024-05-07 19:22:39 +09:00
Junegunn Choi 4bedd33c59 Refactor the code to remove global variables 2024-05-07 16:58:17 +09:00
Junegunn Choi c5fb0c43f9 Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2024-05-07 01:34:35 +09:00
Junegunn Choi 9e4780510e Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2024-05-07 01:26:25 +09:00
Junegunn Choi e8405f40fe Refactor the code so that fzf can be used as a library (#3769) 2024-05-07 01:06:42 +09:00
dependabot[bot] 065b9e6fb2 Bump golang.org/x/term from 0.19.0 to 0.20.0 (#3774)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.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>
2024-05-06 22:51:10 +09:00
dependabot[bot] 98141ca7d8 Bump crate-ci/typos from 1.20.10 to 1.21.0 (#3772)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.10...v1.21.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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>
2024-05-06 22:50:50 +09:00
Junegunn Choi 501577ab28 Fix flaky test case 2024-05-06 13:59:16 +09:00
Junegunn Choi 5669f48343 Do not enable delayed expansion mode when running cmd.exe
And simplify the argument escaping code. Fix #3764.

This may breaks some existing use cases, but the mode causes too much
trouble when escaping arguments and it makes some things not possible.

  # Now you can pass special characters to rg process without any escaping problems: &|<>()@^%!
  fzf --ansi --disabled --bind "change:reload:rg --column --line-number --no-heading --color=always --smart-case -- {q}"

  # No sudden expansion of the arguments on '!'
  fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!" --prompt "&|<>()@^%!"
2024-05-06 13:46:06 +09:00
Junegunn Choi 24ff66d4a9 Fix change-preview reset by change-preview-window
Fix #3770
2024-05-06 09:40:02 +09:00
Junegunn Choi bf184449bc Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2024-05-06 09:27:58 +09:00
Kuremu 7b98c2c653 Add click-header event for reporting clicks within header (#3768)
Sets $FZF_CLICK_HEADER_LINE and $FZF_CLICK_HEADER_COLUMN env vars with
coordinates of the last click inside and relative to the header and
fires click-header event.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-05 18:56:43 +09:00
Junegunn Choi b6add2a257 Fix rendering of preview window border of tcell renderer
(sleep 1; find .) |
    go run -tags tcell main.go --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 17:09:00 +09:00
Junegunn Choi 2bd41f1330 Reduce flicking when changing the size of the preview window with --border
(sleep 1; find .) |
    fzf --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 16:49:30 +09:00
Junegunn Choi c37cd11ca5 Remove unnecessary flicking when changing the size of the preview window
fzf --bind 'space:change-preview-window(60%|70%|80%|90%|hidden|)' --preview 'cat {}'
2024-05-05 11:10:54 +09:00
Junegunn Choi 9dee8edc0c Clear characters on 1-column margin after the preview window on the left 2024-05-05 11:06:50 +09:00
Junegunn Choi f6aa28c380 Fix --info inline-right not properly clearing the previous output
(seq 100000; sleep 1) | fzf --info inline-right --bind load:change-query:x
2024-05-03 12:18:34 +09:00
cyqsimon dba1644518 Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-02 20:52:27 +09:00
Junegunn Choi 260a65b0fb 0.51.0 2024-05-01 14:14:06 +09:00
Junegunn Choi 835d2fb98c [vim] Fix argument escaping for Windows batch file
Fix #3620
2024-05-01 10:02:26 +09:00
Charlie Vieth a9811addaa Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
2024-05-01 09:15:47 +09:00
dependabot[bot] ee9d88b637 Bump crate-ci/typos from 1.20.9 to 1.20.10 (#3757)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.9 to 1.20.10.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.9...v1.20.10)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 23:10:50 +09:00
Junegunn Choi 194a763c46 Escaping for cmd.exe: always use double quotes 2024-04-28 23:30:44 +09:00
Junegunn Choi 8d74446bef Fix escaping for cmd.exe
Close #3651
Close #2609
2024-04-28 22:03:00 +09:00
Junegunn Choi 7ed6c7905c Determine shell type once by the basename 2024-04-28 20:11:05 +09:00
Junegunn Choi 159a37fa37 Restore CmdLine parameter when running commands using cmd.exe 2024-04-28 16:01:19 +09:00
junegunn f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 2024-04-28 00:01:30 +00:00
Junegunn Choi 4a68eac99b Suggest using toggle+up instead of toggle-up 2024-04-27 19:04:30 +09:00
Junegunn Choi 2665580120 Add $FZF_POS environment variable
Close #2175
Close #3753
2024-04-27 18:57:22 +09:00
Junegunn Choi a4391aeedd Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
2024-04-27 18:36:37 +09:00
dependabot[bot] b86a967ee2 Bump crate-ci/typos from 1.19.0 to 1.20.9 (#3749)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.19.0 to 1.20.9.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.19.0...v1.20.9)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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>
2024-04-27 17:57:11 +09:00
Junegunn Choi 608232568b Add 'change-multi' action
Close #3754
2024-04-27 17:38:06 +09:00
Junegunn Choi 7f85beccb5 [completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
2024-04-27 14:14:30 +09:00
Junegunn Choi 767f1255ab Make completion.bash load faster
* Reduce number of `__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)`s
* Clean up options in fzf completion
* Remove telnet completion
2024-04-25 16:54:51 +09:00
Junegunn Choi fddbfe7b0e Fix 'reload' not terminating closed standard input stream
Fix #3750
2024-04-25 16:49:06 +09:00
Junegunn Choi 4ab7fdc28e Merge identical case clauses 2024-04-25 16:34:05 +09:00
Junegunn Choi e352b68878 Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
2024-04-24 18:20:30 +09:00
Junegunn Choi 207deeadba Add -trimpath to build command 2024-04-23 17:24:50 +09:00
Cheng d18d92f925 Replace fmt.Errorf with no parameters with errors.New (#3747) 2024-04-22 09:35:33 +09:00
junegunn af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 2024-04-21 00:01:49 +00:00
Junegunn Choi d8bfb6712d Remove invalid 'result' event when using --sync option
When the search for the initial query doesn't finish immediately
fzf would trigger an invalid 'result' event for an empty query.

  seq 100 | fzf --query 99 --bind result:accept --sync
    # Prints 99

  seq 1000000 | fzf --query 99 --bind result:accept --sync
    # Should print 99, but fzf would print 1
2024-04-20 14:42:43 +09:00
Junegunn Choi f864f8b5f7 Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
2024-04-19 22:40:38 +09:00
Junegunn Choi 31d72efba7 Describe how to build fzf from the latest source using brew 2024-04-18 23:37:12 +09:00
LangLangBart d169c951f3 fix: Move 'emulate' command outside interactive check (#3736) 2024-04-17 18:03:12 +09:00
Junegunn Choi 90d7e38909 [fzf-tmux] Replace command -v with which
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

Fix #3730
2024-04-17 17:29:45 +09:00
hidewrong 938f23e429 Fix typo in comment (#3734)
Signed-off-by: hidewrong <hidewrong@outlook.com>
2024-04-17 17:13:35 +09:00
78 changed files with 4365 additions and 2122 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: crate-ci/typos@v1.19.0 - uses: crate-ci/typos@v1.21.0
+8 -2
View File
@@ -12,6 +12,8 @@ builds:
- darwin - darwin
goarch: goarch:
- amd64 - amd64
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
@@ -19,7 +21,7 @@ builds:
sh -c ' sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"] source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "junegunn.fzf"
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }
@@ -36,6 +38,8 @@ builds:
- darwin - darwin
goarch: goarch:
- arm64 - arm64
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
@@ -43,7 +47,7 @@ builds:
sh -c ' sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"] source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "junegunn.fzf"
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }
@@ -71,6 +75,8 @@ builds:
- 5 - 5
- 6 - 6
- 7 - 7
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
ignore: ignore:
+40 -42
View File
@@ -1,18 +1,17 @@
Advanced fzf examples Advanced fzf examples
====================== ======================
* *Last update: 2024/01/20* * *Last update: 2024/06/06*
* *Requires fzf 0.46.0 or above* * *Requires fzf 0.53.0 or later*
--- ---
<!-- vim-markdown-toc GFM --> <!-- vim-markdown-toc GFM -->
* [Introduction](#introduction) * [Introduction](#introduction)
* [Screen Layout](#screen-layout) * [Display modes](#display-modes)
* [`--height`](#--height) * [`--height`](#--height)
* [`fzf-tmux`](#fzf-tmux) * [`--tmux`](#--tmux)
* [Popup window support](#popup-window-support)
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list) * [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r) * [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
* [Toggling between data sources](#toggling-between-data-sources) * [Toggling between data sources](#toggling-between-data-sources)
@@ -63,7 +62,7 @@ learn its wide variety of features.
This document will guide you through some examples that will familiarize you This document will guide you through some examples that will familiarize you
with the advanced features of fzf. with the advanced features of fzf.
Screen Layout Display modes
------------- -------------
### `--height` ### `--height`
@@ -104,56 +103,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1" export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
``` ```
### `fzf-tmux` ### `--tmux`
Before fzf had `--height` option, we would open fzf in a tmux split pane not (Requires tmux 3.3 or later)
to take up the whole screen. This is done using `fzf-tmux` script.
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
```sh ```sh
# Open fzf on a tmux split pane below the current pane. # Open fzf in a tmux popup at the center of the screen with 70% width and height
# Takes the same set of options. fzf --tmux 70%
fzf-tmux --layout=reverse
``` ```
![image](https://user-images.githubusercontent.com/700826/113379973-f1cc6500-93b5-11eb-8860-c9bc4498aadf.png) ![image](https://github.com/junegunn/fzf/assets/700826/9c365405-c700-49b2-8985-60d822ed4cff)
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike `--tmux` option is silently ignored if you're not on tmux. So if you're trying
`--height` option. But the advantage of it is that it's more flexible. to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
(See `man fzf-tmux` for available options.)
```sh ```sh
# On the right (50%) # --tmux is specified later so it takes precedence over --height when on tmux.
fzf-tmux -r # If you're not on tmux, --tmux is ignored and --height is used instead.
fzf --height 70% --tmux 70%
# On the left (30%)
fzf-tmux -l30%
# Above the cursor
fzf-tmux -u30%
``` ```
![image](https://user-images.githubusercontent.com/700826/113379983-fa24a000-93b5-11eb-93eb-8a3d39b2f163.png) You can also specify the position, width, and height of the popup window in
the following format:
![image](https://user-images.githubusercontent.com/700826/113380001-0577cb80-93b6-11eb-95d0-2ba453866882.png) * `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
![image](https://user-images.githubusercontent.com/700826/113380040-1d4f4f80-93b6-11eb-9bef-737fb120aafe.png)
#### Popup window support
But here's the really cool part; tmux 3.2 added support for popup windows. So
you can open fzf in a popup window, which is quite useful if you frequently
use split panes.
```sh ```sh
# Open tmux in a tmux popup window (default size: 50% of the screen) # 100% width and 60% height
fzf-tmux -p fzf --tmux 100%,60% --border horizontal
# 80% width, 60% height
fzf-tmux -p 80%,60%
``` ```
![image](https://user-images.githubusercontent.com/700826/113380106-4a9bfd80-93b6-11eb-8cee-aeb1c4ce1a1f.png) ![image](https://github.com/junegunn/fzf/assets/700826/f80d3514-d69f-42f2-a8de-a392a562bfcf)
```sh
# On the right (50% width)
fzf --tmux right
```
![image](https://github.com/junegunn/fzf/assets/700826/4033ade4-7efa-421b-a3fb-a430d197098a)
```sh
# On the left (40% width and 70% height)
fzf --tmux left,40%,70%
```
![image](https://github.com/junegunn/fzf/assets/700826/efe43881-2bf0-49ea-ab2e-1377f778cd52)
> [!TIP]
> You might also want to check out my tmux plugins which support this popup > You might also want to check out my tmux plugins which support this popup
> window layout. > window layout.
> >
@@ -536,8 +534,8 @@ pods() {
--bind 'start:reload:$command' \ --bind 'start:reload:$command' \
--bind 'ctrl-r:reload:$command' \ --bind 'ctrl-r:reload:$command' \
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \ --bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \ --bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \ --bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
--preview-window up:follow \ --preview-window up:follow \
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@" --preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
} }
+133
View File
@@ -1,6 +1,139 @@
CHANGELOG CHANGELOG
========= =========
0.53.0
------
- Multi-line display
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
- fzf can now display multi-line items
```sh
# All bash functions, highlighted
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi --highlight-line
# Ripgrep multi-line output
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
```
- To disable multi-line display, use `--no-multi-line`
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
```
╻First line
┃...
╹Last line
```
- Native tmux integration
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
```sh
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
# Center, 100% width and 70% height
fzf --tmux 100%,70% --border horizontal --padding 1,2
# Left, 30% width
fzf --tmux left,30%
# Bottom, 50% height
fzf --tmux bottom,50%
```
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
- To use `--tmux` in Vim plugin:
```vim
let g:fzf_layout = { 'tmux': '100%,70%' }
```
- Added support for endless input streams
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
```sh
# Interactive filtering of a log stream
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
```
- Better Windows Support
- fzf now works on Git bash (mintty) out of the box via winpty integration
- Many fixes and improvements for Windows
- man page is now embedded in the binary; `fzf --man` to see it
- Changed the default `--scroll-off` to 3, as we think it's a better default
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
```sh
# Vim will work fine without /dev/tty redirection
ls | fzf --bind 'space:execute:vim {}' > selected
```
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
```sh
# This doesn't work as expected because --expect is not compatible with --bind
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
# This is something you can do instead
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
```
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
0.52.1
------
- Fixed a critical bug in the Windows version
- Windows users are strongly encouraged to upgrade to this version
0.52.0
------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
- Added color names for selected lines: `selected-fg`, `selected-bg`, and `selected-hl`
```sh
fzf --border --multi --info inline-right --layout reverse --marker ▏ --pointer ▌ --prompt '▌ ' \
--highlight-line --color gutter:-1,selected-bg:238,selected-fg:146,current-fg:189
```
- Added `click-header` event that is triggered when the header section is clicked. When the event is triggered, `$FZF_CLICK_HEADER_COLUMN` and `$FZF_CLICK_HEADER_LINE` are set.
```sh
fd --type f |
fzf --header $'[Files] [Directories]' --header-first \
--bind 'click-header:transform:
(( FZF_CLICK_HEADER_COLUMN <= 7 )) && echo "reload(fd --type f)"
(( FZF_CLICK_HEADER_COLUMN >= 9 )) && echo "reload(fd --type d)"
'
```
- Add `$FZF_COMPLETION_{DIR,PATH}_OPTS` for separately customizing the behavior of fuzzy completion
```sh
# Set --walker options without 'follow' not to follow symbolic links
FZF_COMPLETION_PATH_OPTS="--walker=file,dir,hidden"
FZF_COMPLETION_DIR_OPTS="--walker=dir,hidden"
```
- Fixed Windows argument escaping
- Bug fixes and improvements
- The code was heavily refactored to allow using fzf as a library in Go programs. The API is still experimental and subject to change.
- https://gist.github.com/junegunn/193990b65be48a38aac6ac49d5669170
0.51.0
------
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.
```sh
# Toggle selection to the top or to the bottom
seq 30 | fzf --multi --bind 'load:pos(10)' \
--bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \
--bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done'
```
- Added `--with-shell` option to start child processes with a custom shell command and flags
```sh
gem list | fzf --with-shell 'ruby -e' \
--preview 'pp Gem::Specification.find_by_name({1})' \
--bind 'ctrl-o:execute-silent:
spec = Gem::Specification.find_by_name({1})
[spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
system "open", _1
end
'
```
- Added `change-multi` action for dynamically changing `--multi` option
- `change-multi` - enable multi-select mode with no limit
- `change-multi(NUM)` - enable multi-select mode with a limit
- `change-multi(0)` - disable multi-select mode
- Windows improvements
- `become` action is now supported on Windows
- Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
- Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes.
- Bug fixes and improvements
0.50.0 0.50.0
------ ------
- Search performance optimization. You can observe 50%+ improvement in some scenarios. - Search performance optimization. You can observe 50%+ improvement in some scenarios.
+2 -2
View File
@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 ubuntu:22.04 FROM ubuntu:24.04
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.22.3 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile RUN echo '. ~/.bashrc' >> ~/.bash_profile
+7 -8
View File
@@ -1,10 +1,10 @@
SHELL := bash SHELL := bash
GO ?= go GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)
@@ -25,7 +25,7 @@ endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION) $(error Not on git repository; cannot determine $$FZF_REVISION)
endif endif
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
@@ -79,7 +79,6 @@ all: target/$(BINARY)
test: $(SOURCES) test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf \
github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \ github.com/junegunn/fzf/src/tui \
@@ -174,12 +173,12 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf
docker: docker:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch tmux docker run -it fzf-ubuntu tmux
docker-test: docker-test:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch docker run -it fzf-ubuntu
update: update:
$(GO) get -u $(GO) get -u
+5 -4
View File
@@ -294,7 +294,7 @@ The following table summarizes the available options.
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) | | `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) | | `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
@@ -457,12 +457,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
``` ```
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key. or above) by putting `--tmux` option value in `tmux` key.
```vim ```vim
" See `man fzf-tmux` for available options " See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif
+108 -100
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -7,7 +7,7 @@ fail() {
exit 2 exit 2
} }
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found' [[ -x "$fzf" ]] || fail 'fzf executable not found'
args=() args=()
@@ -132,8 +132,10 @@ if [[ -z "$TMUX" ]]; then
exit $? exit $?
fi fi
# --height option is not allowed. CTRL-Z is also disabled. # * --height option is not allowed
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore") # * CTRL-Z is also disabled
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
# Handle zoomed tmux pane without popup options by moving it to a temp window # Handle zoomed tmux pane without popup options by moving it to a temp window
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
+5 -4
View File
@@ -311,7 +311,7 @@ The following table summarizes the available options.
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` ) `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` ) `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` ) `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
---------------------------+---------------+---------------------------------------------------------------------- ---------------------------+---------------+----------------------------------------------------------------------
@@ -469,11 +469,12 @@ in Neovim.
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
< <
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2 Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
or above) by putting fzf-tmux options in `tmux` key. or above) by putting `--tmux` options in `tmux` key.
> >
" See `man fzf-tmux` for available options " See `--tmux` option in `man fzf` for available options
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
if exists('$TMUX') if exists('$TMUX')
let g:fzf_layout = { 'tmux': '-p90%,60%' } let g:fzf_layout = { 'tmux': '90%,70%' }
else else
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } } let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
endif endif
+2 -2
View File
@@ -6,8 +6,8 @@ require (
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.19.0 golang.org/x/sys v0.20.0
golang.org/x/term v0.19.0 golang.org/x/term v0.20.0
) )
require ( require (
+4 -4
View File
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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=
+8 -4
View File
@@ -2,7 +2,7 @@
set -u set -u
version=0.50.0 version=0.53.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() { try_curl() {
command -v curl > /dev/null && command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf - curl -fL $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -125,7 +125,7 @@ try_curl() {
try_wget() { try_wget() {
command -v wget > /dev/null && command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf - wget -O - $1 | tar --no-same-owner -xzf -
else else
local temp=${TMPDIR:-/tmp}/fzf.zip local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -265,7 +265,11 @@ fi
EOF EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
echo "eval \"\$(fzf --$shell)\"" >> "$src" if [[ "$shell" = zsh ]]; then
echo "source <(fzf --$shell)" >> "$src"
else
echo "eval \"\$(fzf --$shell)\"" >> "$src"
fi
else else
cat >> "$src" << EOF cat >> "$src" << EOF
# Auto-completion # Auto-completion
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.50.0" $version="0.53.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+50 -4
View File
@@ -3,14 +3,16 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"os"
"os/exec"
"strings" "strings"
fzf "github.com/junegunn/fzf/src" fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.50" var version = "0.53"
var revision string = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash
var bashKeyBindings []byte var bashKeyBindings []byte
@@ -27,15 +29,30 @@ var zshCompletion []byte
//go:embed shell/key-bindings.fish //go:embed shell/key-bindings.fish
var fishKeyBindings []byte var fishKeyBindings []byte
//go:embed man/man1/fzf.1
var manPage []byte
func printScript(label string, content []byte) { func printScript(label string, content []byte) {
fmt.Println("### " + label + " ###") fmt.Println("### " + label + " ###")
fmt.Println(strings.TrimSpace(string(content))) fmt.Println(strings.TrimSpace(string(content)))
fmt.Println("### end: " + label + " ###") fmt.Println("### end: " + label + " ###")
} }
func exit(code int, err error) {
if code == fzf.ExitError {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() { func main() {
protector.Protect() protector.Protect()
options := fzf.ParseOptions()
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash { if options.Bash {
printScript("key-bindings.bash", bashKeyBindings) printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion) printScript("completion.bash", bashCompletion)
@@ -51,5 +68,34 @@ func main() {
fmt.Println("fzf_key_bindings") fmt.Println("fzf_key_bindings")
return return
} }
fzf.Run(options, version, revision) if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
if options.Man {
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
if len(file) == 0 {
fmt.Print(string(manPage))
return
}
defer os.Remove(file)
cmd := exec.Command("man", file)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Print(string(manPage))
}
return
}
code, err := fzf.Run(options)
exit(code, err)
} }
-158
View File
@@ -1,158 +0,0 @@
package main
import (
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var pkgs []*build.Package
seen := make(map[string]bool)
err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
return filepath.SkipDir
}
return nil
}
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path)
if !seen[dir] {
pkg, err := build.ImportDir(dir, build.ImportComment)
if err != nil {
return fmt.Errorf("%s: %s", dir, err)
}
if pkg.ImportPath == "" || pkg.ImportPath == "." {
importPath, err := filepath.Rel(wd, dir)
if err != nil {
t.Fatal(err)
}
pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
}
pkgs = append(pkgs, pkg)
seen[dir] = true
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].ImportPath < pkgs[j].ImportPath
})
return pkgs
}
var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
var files []*ast.File
fset := token.NewFileSet()
for _, name := range bpkg.GoFiles {
filename := filepath.Join(bpkg.Dir, name)
af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files = append(files, af)
}
info := types.Info{
Uses: make(map[*ast.Ident]types.Object),
}
conf := types.Config{
Importer: sourceImporter,
}
_, err := conf.Check(bpkg.Name, fset, files, &info)
if err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for id, obj := range info.Uses {
if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
pos := fset.Position(id.Pos())
name, err := filepath.Rel(wd, pos.Filename)
if err != nil {
t.Log(err)
name = pos.Filename
}
name = filepath.ToSlash(name)
// Check if the usage is allowed
if allowed[name] > 0 {
allowed[name]--
continue
}
t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
errOsExit = true
}
}
return errOsExit
}
// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
// references to it anywhere else in the fzf code base.
func TestOSExitNotAllowed(t *testing.T) {
if testing.Short() {
t.Skip("skipping: short test")
}
allowed := map[string]int{
"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
}
var errOsExit bool
for _, pkg := range loadPackages(t) {
t.Run(pkg.ImportPath, func(t *testing.T) {
if checkPackageForOsExit(t, pkg, allowed) {
errOsExit = true
}
})
}
if t.Failed() && errOsExit {
var names []string
for name := range allowed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
const errMsg = `
Test failed because os.Exit was referenced outside of the following files:
%s
Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
This is enforced because calling os.Exit() prevents the functions
registered with util.AtExit() from running.`
t.Errorf(errMsg, strings.Join(names, "\n "))
}
}
+1 -1
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 "Apr 2024" "fzf 0.50.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Jun 2024" "fzf 0.53.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
+137 -28
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 "Apr 2024" "fzf 0.50.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jun 2024" "fzf 0.53.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -30,7 +30,10 @@ fzf - a command-line fuzzy finder
fzf [options] fzf [options]
.SH DESCRIPTION .SH DESCRIPTION
fzf is a general-purpose command-line fuzzy finder. fzf is an interactive filter program for any kind of list.
It implements a "fuzzy" matching algorithm, so you can quickly type in patterns
with omitted characters and still get the results you want.
.SH OPTIONS .SH OPTIONS
.SS Note .SS Note
@@ -46,10 +49,10 @@ it with \fB+x\fR or \fB--no-extended\fR.
.B "-e, --exact" .B "-e, --exact"
Enable exact-match Enable exact-match
.TP .TP
.B "-i" .B "-i, --ignore-case"
Case-insensitive match (default: smart-case match) Case-insensitive match (default: smart-case match)
.TP .TP
.B "+i" .B "+i, --no-ignore-case"
Case-sensitive match Case-sensitive match
.TP .TP
.B "--literal" .B "--literal"
@@ -58,14 +61,32 @@ Do not normalize latin script letters for matching.
.BI "--scheme=" SCHEME .BI "--scheme=" SCHEME
Choose scoring scheme tailored for different types of input. Choose scoring scheme tailored for different types of input.
.br .RS
.BR default " Generic scoring scheme designed to work well with any type of input" .B default
.br .RS
.BR path " Scoring scheme well suited for file paths Generic scoring scheme designed to work well with any type of input.
.br .RE
.BR history " Scoring scheme well suited for command history or any input where chronological ordering is important .RE
Sets \fB--tiebreak=index\fR as well.
.br .RS
.B path
.RS
Additional bonus point is only given to the characters after path separator.
You might want to choose this scheme over \fBdefault\fR if you have many files
with spaces in their paths.
.RE
.RE
.RS
.B history
.RS
Scoring scheme well suited for command history or any input where chronological
ordering is important. No additional bonus points are given so that we give
more weight to the chronological ordering. This also sets
\fB--tiebreak=index\fR.
.RE
.RE
.TP .TP
.BI "--algo=" TYPE .BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2) Fuzzy matching algorithm (default: v2)
@@ -85,7 +106,8 @@ See \fBFIELD INDEX EXPRESSION\fR for the details.
Transform the presentation of each line using field index expressions Transform the presentation of each line using field index expressions
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR, \fB--with-nth\fR, and field index
expressions (default: AWK-style)
.TP .TP
.BI "--disabled" .BI "--disabled"
Do not perform search. With this option, fzf becomes a simple selector Do not perform search. With this option, fzf becomes a simple selector
@@ -96,6 +118,16 @@ interface rather than a "fuzzy finder". You can later enable the search using
.B "+s, --no-sort" .B "+s, --no-sort"
Do not sort the result Do not sort the result
.TP .TP
.B "--tail=NUM"
Maximum number of items to keep in memory. This is useful when you want to
browse an endless stream of data (e.g. log stream) with fzf while limiting
memory usage.
.RS
e.g.
\fB# Interactive filtering of a log stream
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact\fR
.RE
.TP
.B "--track" .B "--track"
Make fzf track the current selection when the result list is updated. Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled. It is This can be useful when browsing logs using fzf with sorting disabled. It is
@@ -162,13 +194,16 @@ the details.
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
.B "--no-multi-line"
Disable multi-line display of items when using \fB--read0\fR
.TP
.B "--keep-right" .B "--keep-right"
Keep the right end of the line visible when it's too long. Effective only when Keep the right end of the line visible when it's too long. Effective only when
the query string is empty. the query string is empty.
.TP .TP
.BI "--scroll-off=" "LINES" .BI "--scroll-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to Number of screen lines to keep above or below when scrolling to the top or to
the bottom (default: 0). the bottom (default: 3).
.TP .TP
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
@@ -204,17 +239,41 @@ height minus the given value.
fzf --height=-1 fzf --height=-1
When prefixed with \fB~\fR, fzf will automatically determine the height in the 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 range according to the input size.
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 # Will not take up 100% of the screen
seq 5 | fzf --height=~100% seq 5 | fzf --height=~100%
Adaptive height has the following limitations:
.br
* Cannot be used with top/bottom margin and padding given in percent size
.br
* Negative value is not allowed
.br
* It will not find the right size when there are multi-line items
.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).
Ignored when \fB--height\fR is not specified. Ignored when \fB--height\fR is not specified.
.TP
.BI "--tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]]"
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
later. This option is ignored if you are not running fzf inside tmux.
e.g.
\fB# Popup in the center with 70% width and height
fzf --tmux 70%
# Popup on the left with 40% width and 100% height
fzf --tmux right,40%
# Popup on the bottom with 100% width and 30% height
fzf --tmux bottom,30%
# Popup on the top with 80% width and 40% height
fzf --tmux top,80%,40%\fR
.TP .TP
.BI "--layout=" "LAYOUT" .BI "--layout=" "LAYOUT"
Choose the layout (default: default) Choose the layout (default: default)
@@ -420,10 +479,14 @@ Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
Input prompt (default: '> ') Input prompt (default: '> ')
.TP .TP
.BI "--pointer=" "STR" .BI "--pointer=" "STR"
Pointer to the current line (default: '>') Pointer to the current line (default: '▌' or '>' depending on \fB--no-unicode\fR)
.TP .TP
.BI "--marker=" "STR" .BI "--marker=" "STR"
Multi-select marker (default: '>') Multi-select marker (default: '┃' or '>' depending on \fB--no-unicode\fR)
.TP
.BI "--marker-multi-line=" "STR"
Multi-select marker for multi-line entries. 3 elements for top, middle, and bottom.
(default: '╻┃╹' or '.|'' depending on \fB--no-unicode\fR)
.TP .TP
.BI "--header=" "STR" .BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed The given string will be printed as the sticky header. The lines are displayed
@@ -455,7 +518,7 @@ color mappings.
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
(default: dark on 256-color terminal, otherwise 16) (default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
\fBdark \fRColor scheme for dark 256-color terminal \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal \fBlight \fRColor scheme for light 256-color terminal
@@ -464,14 +527,17 @@ color mappings.
.B COLOR NAMES: .B COLOR NAMES:
\fBfg \fRText \fBfg \fRText
\fBselected-fg \fRSelected line text
\fBpreview-fg \fRPreview window text \fBpreview-fg \fRPreview window text
\fBbg \fRBackground \fBbg \fRBackground
\fBselected-bg \fRSelected line background
\fBpreview-bg \fRPreview window background \fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line) \fBselected-hl \fRHighlighted substrings in the selected line
\fBbg+ \fRBackground (current line) \fBcurrent-fg (fg+) \fRText (current line)
\fBcurrent-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line) \fBcurrent-hl (hl+) \fRHighlighted substrings (current line)
\fBquery \fRQuery string \fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR) \fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters) \fBinfo \fRInfo line (match counters)
@@ -534,6 +600,9 @@ color mappings.
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR --color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE .RE
.TP .TP
.B "--highlight-line"
Highlight the whole current line
.TP
.B "--no-bold" .B "--no-bold"
Do not use bold text Do not use bold text
.TP .TP
@@ -788,6 +857,14 @@ list.
e.g. e.g.
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE .RE
This option is not compatible with \fB--bind\fR on the same key and will take
precedence over it. To combine the two, use \fBprint\fR action.
.RS
e.g.
\fBfzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'\fR
.RE
.TP .TP
.B "--read0" .B "--read0"
Read input delimited by ASCII NUL characters instead of newline characters Read input delimited by ASCII NUL characters instead of newline characters
@@ -812,12 +889,22 @@ e.g.
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. the finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--with-shell=STR"
Shell command and flags to start child processes with. On *nix Systems, the
default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR.
On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
set.
.RS
e.g. \fBgem list | fzf --with-shell 'ruby -e' --preview 'pp Gem::Specification.find_by_name({1})'\fR
.RE
.TP
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]" .B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method. to send actions to perform via POST method.
@@ -855,9 +942,16 @@ e.g.
# Choose port automatically and export it as $FZF_PORT to the child process # Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port' fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR \fR
.SS Help
.TP .TP
.B "--version" .B "--version"
Display version information and exit Display version information and exit
.TP
.B "--help"
Show help message
.TP
.B "--man"
Show man page
.SS Directory traversal .SS Directory traversal
.TP .TP
@@ -895,7 +989,7 @@ e.g. \fBeval "$(fzf --bash)"\fR
.B "--zsh" .B "--zsh"
Print script to set up Zsh shell integration Print script to set up Zsh shell integration
e.g. \fBeval "$(fzf --zsh)"\fR e.g. \fBsource <(fzf --zsh)\fR
.TP .TP
.B "--fish" .B "--fish"
@@ -932,6 +1026,10 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br .br
.BR 2 " Error" .BR 2 " Error"
.br .br
.BR 126 " Permission denied error from \fBbecome\fR action"
.br
.BR 127 " Invalid shell command for \fBbecome\fR action"
.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION .SH FIELD INDEX EXPRESSION
@@ -972,6 +1070,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_SELECT_COUNT " Number of selected items" .BR FZF_SELECT_COUNT " Number of selected items"
.br .br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_QUERY " Current query string" .BR FZF_QUERY " Current query string"
.br .br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
@@ -1264,6 +1364,15 @@ e.g.
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR \fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
.RE .RE
\fIclick-header\fR
.RS
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
e.g.
\fBprintf "head1\\nhead2" | fzf --header-lines=2 --bind 'click-header:transform-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
.RE
.SS AVAILABLE ACTIONS: .SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions. A key or an event can be bound to one or more of the following actions.
@@ -1282,6 +1391,8 @@ A key or an event can be bound to one or more of the following actions.
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR) \fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-multi\fR (enable multi-select mode with no limit)
\fBchange-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) \fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
@@ -1331,7 +1442,7 @@ A key or an event can be bound to one or more of the following actions.
\fBpreview-half-page-up\fR \fBpreview-half-page-up\fR
\fBpreview-bottom\fR \fBpreview-bottom\fR
\fBpreview-top\fR \fBpreview-top\fR
\fBprint-query\fR (print query and exit) \fBprint(...)\fR (add string to the output queue and print on exit)
\fBput\fR (put the character to the prompt) \fBput\fR (put the character to the prompt)
\fBput(...)\fR (put the given string to the prompt) \fBput(...)\fR (put the given string to the prompt)
\fBrefresh-preview\fR \fBrefresh-preview\fR
@@ -1439,8 +1550,6 @@ call.
\fBfzf --bind "enter:become(vim {})"\fR \fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list
+48 -44
View File
@@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage) return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction endfunction
function! s:wrap_cmds(cmds) function! s:wrap_cmds(cmds)
return map([ return map(['@echo off']
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
\ + (has('gui_running') ? ['set TERM= > nul'] : []) \ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) \ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ + ['endlocal'],
\ '<SID>enc_to_cp(v:val."\r")') \ '<SID>enc_to_cp(v:val."\r")')
endfunction endfunction
else else
@@ -84,11 +81,21 @@ else
endif endif
function! s:shellesc_cmd(arg) function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let e = '"'
let escaped = substitute(escaped, '%', '%%', 'g') let slashes = 0
let escaped = substitute(escaped, '"', '\\^&', 'g') for c in split(a:arg, '\zs')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') if c ==# '\'
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' let slashes += 1
elseif c ==# '"'
let e .= repeat('\', slashes + 1)
let slashes = 0
else
let slashes = 0
endif
let e .= c
endfor
let e .= repeat('\', slashes) .'"'
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
endfunction endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)
@@ -320,7 +327,10 @@ function! s:common_sink(action, lines) abort
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`) " the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h') let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
for item in a:lines for item in a:lines
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/') if has('win32unix') && item !~ '/'
let item = substitute(item, '\', '/', 'g')
end
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
let sep = s:is_win ? '\' : '/' let sep = s:is_win ? '\' : '/'
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep) let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
endif endif
@@ -480,6 +490,8 @@ function! s:extract_option(opts, name)
return opt return opt
endfunction endfunction
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh() let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
@@ -501,19 +513,19 @@ try
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
let source = remove(dict, 'source') let source = dict.source
let type = type(source) let type = type(source)
if type == 1 if type == 1
let source_command = source let prefix = '('.source.')|'
elseif type == 3 elseif type == 3
let temps.input = s:fzf_tempname() let temps.input = s:fzf_tempname()
call s:writefile(source, temps.input) call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input) let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else else
throw 'Invalid source type' throw 'Invalid source type'
endif endif
else else
let source_command = '' let prefix = ''
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux') let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@@ -522,26 +534,23 @@ try
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term || let use_term = has_nvim_term || has_vim8_term
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window')) \ && !s:need_cmd_window
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled() let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
let use_term = 0 let use_term = 0
endif endif
if use_term if use_term
let optstr .= ' --no-height' let optstr .= ' --no-height --no-tmux'
elseif use_height elseif use_height
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height let optstr .= ' --no-tmux --height='.height
endif endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options' " Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr]) let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
@@ -552,14 +561,6 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote] let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry endtry
endfunction endfunction
@@ -578,19 +579,21 @@ function! s:fzf_tmux(dict)
if empty(size) if empty(size)
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let spec = a:dict[o] let size = o . ',' . a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif
break break
endif endif
endfor endfor
endif endif
return printf('LINES=%d COLUMNS=%d %s %s - --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size) " Legacy fzf-tmux options
if size =~ '-'
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
end
" Using native --tmux option
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
@@ -713,14 +716,15 @@ function! s:execute(dict, command, use_height, temps) abort
call jobstart(cmd, fzf) call jobstart(cmd, fzf)
return [] return []
endif endif
elseif has('win32unix') && $TERM !=# 'cygwin' elseif s:need_cmd_window
let shellscript = s:fzf_tempname() let shellscript = s:fzf_tempname()
call s:writefile([command], shellscript) call s:writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) let command = 'start //WAIT sh -c '.shellscript
let a:temps.shellscript = shellscript let a:temps.shellscript = shellscript
endif endif
if a:use_height if a:use_height
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command)) let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else else
execute 'silent !'.command execute 'silent !'.command
endif endif
+117 -132
View File
@@ -4,10 +4,12 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.bash # /_/ /___/_/ completion.bash
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty) # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
@@ -32,6 +34,14 @@ if [[ $- =~ i ]]; then
# To redraw line after fzf closes (printf '\e[5n') # To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
@@ -91,129 +101,86 @@ _fzf_opts_completion() {
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-h --help
-x --extended
-e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo
--scheme
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth
--with-nth
-s --sort
+s --no-sort
--track
--no-track
--tac
--no-tac
-i
+i
-m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse
+c --no-color +c --no-color
+2 --no-256 +i --no-ignore-case
--black +s --no-sort
--no-black +x --no-extended
--bold --ansi
--no-bold --bash
--layout --bind
--reverse
--no-reverse
--cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels
-1 --select-1
+1 --no-select-1
-0 --exit-0
+0 --no-exit-0
--read0
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt
--pointer
--marker
--sync
--no-sync
--async
--no-history
--history
--history-size
--no-header
--no-header-lines
--header
--header-lines
--header-first
--no-header-first
--ellipsis
--preview
--no-preview
--preview-window
--height
--min-height
--no-height
--no-margin
--no-padding
--no-border
--border --border
--no-border-label
--border-label --border-label
--border-label-pos --border-label-pos
--no-preview-label --color
--cycle
--disabled
--ellipsis
--expect
--filepath-word
--fish
--header
--header-first
--header-lines
--height
--highlight-line
--history
--history-size
--hscroll-off
--info
--jump-labels
--keep-right
--layout
--listen
--listen-unsafe
--literal
--man
--margin
--marker
--min-height
--no-bold
--no-clear
--no-hscroll
--no-mouse
--no-scrollbar
--no-separator
--no-unicode
--padding
--pointer
--preview
--preview-label --preview-label
--preview-label-pos --preview-label-pos
--no-unicode --preview-window
--unicode --print-query
--margin --print0
--padding --prompt
--read0
--reverse
--scheme
--scroll-off
--separator
--sync
--tabstop --tabstop
--listen --tac
--no-listen --tiebreak
--clear --tmux
--no-clear --track
--version --version
--with-nth
--with-shell
--zsh
-0 --exit-0
-1 --select-1
-d --delimiter
-e --exact
-f --filter
-h --help
-i --ignore-case
-m --multi
-n --nth
-q --query
--" --"
case "${prev}" in case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme) --scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") ) COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0 return 0
@@ -335,13 +302,19 @@ __fzf_generic_path_completion() {
[[ -z "$dir" ]] && dir='.' [[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$( matches=$(
unset FZF_DEFAULT_COMMAND export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -F "$1" > /dev/null; then if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else else
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden if [[ $1 =~ dir ]]; then
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do fi | while read -r item; do
printf "%q " "${item%$3}$3" printf "%q " "${item%$3}$3"
done done
@@ -399,7 +372,10 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ') selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then if [[ -n "$selected" ]]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -503,8 +479,11 @@ complete -o default -F _fzf_opts_completion fzf
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch. # fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
a_cmds="
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
# undocumented and subject to change in the future.
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
awk bat cat diff diff3 awk bat cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
@@ -512,10 +491,11 @@ a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp ln ls mv open rm rsync scp
svn tar unzip zip" svn tar unzip zip"}"
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
# Preserve existing completion # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
if type _comp_load > /dev/null 2>&1; then if type _comp_load > /dev/null 2>&1; then
# _comp_load was added in bash-completion 2.12 to replace _completion_loader. # _comp_load was added in bash-completion 2.12 to replace _completion_loader.
@@ -551,10 +531,21 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
done done
# Variables
for cmd in $v_cmds; do
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
done
# Aliases
__fzf_defc unalias _fzf_alias_completion "-a"
# Processes
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
# ssh # ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault" __fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds unset cmd d_cmds a_cmds v_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
local kind fn cmd local kind fn cmd
@@ -576,10 +567,4 @@ _fzf_setup_completion() {
done done
} }
# Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill
fi fi
+33 -14
View File
@@ -4,12 +4,12 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/ completion.zsh # /_/ /___/_/ completion.zsh
# #
# - $FZF_TMUX (default: 0) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%') # - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
if [[ -o interactive ]]; then # - $FZF_COMPLETION_DIR_OPTS (default: empty)
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
@@ -75,6 +75,9 @@ fi
# This brace is the start of try-always block. The `always` part is like # This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options. # `finally` in lesser languages. We use it to *always* restore user options.
{ {
# The 'emulate' command should not be placed inside the interactive if check;
# placing it there fails to disable alias expansion. See #3731.
if [[ -o interactive ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
# #
@@ -93,6 +96,14 @@ fi
########################################################### ###########################################################
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
@@ -146,13 +157,19 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$( matches=$(
unset FZF_DEFAULT_COMMAND export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else else
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden if [[ $compgen =~ dir ]]; then
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do fi | while read item; do
item="${item%$suffix}$suffix" item="${item%$suffix}$suffix"
echo -n "${(q)item} " echo -n "${(q)item} "
@@ -217,7 +234,10 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo" _fzf_feed_fifo "$fifo"
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
@@ -315,7 +335,7 @@ fzf-completion() {
# Trigger sequence given # Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@@ -345,11 +365,10 @@ fzf-completion() {
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion
fi
} always { } always {
# Restore the original options. # Restore the original options.
eval $__fzf_completion_options eval $__fzf_completion_options
'unset' '__fzf_completion_options' 'unset' '__fzf_completion_options'
} }
fi
+24 -14
View File
@@ -16,10 +16,19 @@ if [[ $- =~ i ]]; then
# Key bindings # Key bindings
# ------------ # ------------
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_select__() { __fzf_select__() {
local opts FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m" FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" | FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
while read -r item; do while read -r item; do
printf '%q ' "$item" # escape special chars printf '%q ' "$item" # escape special chars
done done
@@ -37,25 +46,26 @@ fzf-file-widget() {
} }
__fzf_cd__() { __fzf_cd__() {
local opts dir local dir
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
dir=$( dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)" ) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
} }
if command -v perl > /dev/null; then if command -v perl > /dev/null; then
__fzf_history__() { __fzf_history__() {
local output opts script local output script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0" script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" | last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then
echo "$READLINE_LINE" echo "$READLINE_LINE"
else else
@@ -64,14 +74,13 @@ if command -v perl > /dev/null; then
} }
else # awk - fallback for POSIX systems else # awk - fallback for POSIX systems
__fzf_history__() { __fzf_history__() {
local output opts script n x y z d local output script n x y z d
if [[ -z $__fzf_awk ]]; then if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk __fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4 # choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null) IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk [[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries [[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } } script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next } NR==1 { b = substr($0, 2); next }
@@ -82,7 +91,8 @@ else # awk - fallback for POSIX systems
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )* builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )* command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then
+26 -8
View File
@@ -18,6 +18,15 @@ status is-interactive; or exit 0
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
end
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
@@ -27,8 +36,9 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
@@ -49,8 +59,6 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
@@ -58,10 +66,19 @@ function fzf_key_bindings
# history's -z flag was added in fish 2.4.0, so don't use it for versions # history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0. # before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result if type -P perl > /dev/null 2>&1
and commandline -- $result set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
and commandline -- $result
else
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
and commandline -- $result
end
else else
history | eval (__fzfcmd) -q '(commandline)' | read -l result builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result and commandline -- $result
end end
end end
@@ -76,12 +93,13 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ] if [ -n "$result" ]
cd -- $result builtin cd -- $result
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""
+33 -13
View File
@@ -11,8 +11,6 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
if [[ -o interactive ]]; then
# Key bindings # Key bindings
# ------------ # ------------
@@ -38,12 +36,23 @@ fi
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{ {
if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@@ -57,7 +66,7 @@ __fzfcmd() {
} }
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$? local ret=$?
zle reset-prompt zle reset-prompt
return $ret return $ret
@@ -72,7 +81,10 @@ fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)" local dir="$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
@@ -96,13 +108,22 @@ fi
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | # Ensure the associative history array, which maps event numbers to the full
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))" # history lines, is loaded, and that Perl is installed for multi-line output.
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
selected="$(printf '%1$s\t%2$s\000' "${(vk)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++) { s/\n/\n\t/gm; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
else
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected") if num=$(awk '{print $1; exit}' <<< "$selected" | grep -o '^[1-9][0-9]*'); then
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then zle vi-fetch-history -n $num
zle vi-fetch-history -n ${num%\*}
else # selected is a custom query, not from history else # selected is a custom query, not from history
LBUFFER="$selected" LBUFFER="$selected"
fi fi
@@ -114,10 +135,9 @@ zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget bindkey -M viins '^R' fzf-history-widget
fi
} always { } always {
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options
'unset' '__fzf_key_bindings_options' 'unset' '__fzf_key_bindings_options'
} }
fi
+96 -93
View File
@@ -26,102 +26,105 @@ func _() {
_ = x[actCancel-15] _ = x[actCancel-15]
_ = x[actChangeBorderLabel-16] _ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17] _ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18] _ = x[actChangeMulti-18]
_ = x[actChangePrompt-19] _ = x[actChangePreviewLabel-19]
_ = x[actChangeQuery-20] _ = x[actChangePrompt-20]
_ = x[actClearScreen-21] _ = x[actChangeQuery-21]
_ = x[actClearQuery-22] _ = x[actClearScreen-22]
_ = x[actClearSelection-23] _ = x[actClearQuery-23]
_ = x[actClose-24] _ = x[actClearSelection-24]
_ = x[actDeleteChar-25] _ = x[actClose-25]
_ = x[actDeleteCharEof-26] _ = x[actDeleteChar-26]
_ = x[actEndOfLine-27] _ = x[actDeleteCharEof-27]
_ = x[actForwardChar-28] _ = x[actEndOfLine-28]
_ = x[actForwardWord-29] _ = x[actFatal-29]
_ = x[actKillLine-30] _ = x[actForwardChar-30]
_ = x[actKillWord-31] _ = x[actForwardWord-31]
_ = x[actUnixLineDiscard-32] _ = x[actKillLine-32]
_ = x[actUnixWordRubout-33] _ = x[actKillWord-33]
_ = x[actYank-34] _ = x[actUnixLineDiscard-34]
_ = x[actBackwardKillWord-35] _ = x[actUnixWordRubout-35]
_ = x[actSelectAll-36] _ = x[actYank-36]
_ = x[actDeselectAll-37] _ = x[actBackwardKillWord-37]
_ = x[actToggle-38] _ = x[actSelectAll-38]
_ = x[actToggleSearch-39] _ = x[actDeselectAll-39]
_ = x[actToggleAll-40] _ = x[actToggle-40]
_ = x[actToggleDown-41] _ = x[actToggleSearch-41]
_ = x[actToggleUp-42] _ = x[actToggleAll-42]
_ = x[actToggleIn-43] _ = x[actToggleDown-43]
_ = x[actToggleOut-44] _ = x[actToggleUp-44]
_ = x[actToggleTrack-45] _ = x[actToggleIn-45]
_ = x[actToggleTrackCurrent-46] _ = x[actToggleOut-46]
_ = x[actToggleHeader-47] _ = x[actToggleTrack-47]
_ = x[actTrackCurrent-48] _ = x[actToggleTrackCurrent-48]
_ = x[actUntrackCurrent-49] _ = x[actToggleHeader-49]
_ = x[actDown-50] _ = x[actTrackCurrent-50]
_ = x[actUp-51] _ = x[actUntrackCurrent-51]
_ = x[actPageUp-52] _ = x[actDown-52]
_ = x[actPageDown-53] _ = x[actUp-53]
_ = x[actPosition-54] _ = x[actPageUp-54]
_ = x[actHalfPageUp-55] _ = x[actPageDown-55]
_ = x[actHalfPageDown-56] _ = x[actPosition-56]
_ = x[actOffsetUp-57] _ = x[actHalfPageUp-57]
_ = x[actOffsetDown-58] _ = x[actHalfPageDown-58]
_ = x[actJump-59] _ = x[actOffsetUp-59]
_ = x[actJumpAccept-60] _ = x[actOffsetDown-60]
_ = x[actPrintQuery-61] _ = x[actJump-61]
_ = x[actRefreshPreview-62] _ = x[actJumpAccept-62]
_ = x[actReplaceQuery-63] _ = x[actPrintQuery-63]
_ = x[actToggleSort-64] _ = x[actRefreshPreview-64]
_ = x[actShowPreview-65] _ = x[actReplaceQuery-65]
_ = x[actHidePreview-66] _ = x[actToggleSort-66]
_ = x[actTogglePreview-67] _ = x[actShowPreview-67]
_ = x[actTogglePreviewWrap-68] _ = x[actHidePreview-68]
_ = x[actTransform-69] _ = x[actTogglePreview-69]
_ = x[actTransformBorderLabel-70] _ = x[actTogglePreviewWrap-70]
_ = x[actTransformHeader-71] _ = x[actTransform-71]
_ = x[actTransformPreviewLabel-72] _ = x[actTransformBorderLabel-72]
_ = x[actTransformPrompt-73] _ = x[actTransformHeader-73]
_ = x[actTransformQuery-74] _ = x[actTransformPreviewLabel-74]
_ = x[actPreview-75] _ = x[actTransformPrompt-75]
_ = x[actChangePreview-76] _ = x[actTransformQuery-76]
_ = x[actChangePreviewWindow-77] _ = x[actPreview-77]
_ = x[actPreviewTop-78] _ = x[actChangePreview-78]
_ = x[actPreviewBottom-79] _ = x[actChangePreviewWindow-79]
_ = x[actPreviewUp-80] _ = x[actPreviewTop-80]
_ = x[actPreviewDown-81] _ = x[actPreviewBottom-81]
_ = x[actPreviewPageUp-82] _ = x[actPreviewUp-82]
_ = x[actPreviewPageDown-83] _ = x[actPreviewDown-83]
_ = x[actPreviewHalfPageUp-84] _ = x[actPreviewPageUp-84]
_ = x[actPreviewHalfPageDown-85] _ = x[actPreviewPageDown-85]
_ = x[actPrevHistory-86] _ = x[actPreviewHalfPageUp-86]
_ = x[actPrevSelected-87] _ = x[actPreviewHalfPageDown-87]
_ = x[actPut-88] _ = x[actPrevHistory-88]
_ = x[actNextHistory-89] _ = x[actPrevSelected-89]
_ = x[actNextSelected-90] _ = x[actPrint-90]
_ = x[actExecute-91] _ = x[actPut-91]
_ = x[actExecuteSilent-92] _ = x[actNextHistory-92]
_ = x[actExecuteMulti-93] _ = x[actNextSelected-93]
_ = x[actSigStop-94] _ = x[actExecute-94]
_ = x[actFirst-95] _ = x[actExecuteSilent-95]
_ = x[actLast-96] _ = x[actExecuteMulti-96]
_ = x[actReload-97] _ = x[actSigStop-97]
_ = x[actReloadSync-98] _ = x[actFirst-98]
_ = x[actDisableSearch-99] _ = x[actLast-99]
_ = x[actEnableSearch-100] _ = x[actReload-100]
_ = x[actSelect-101] _ = x[actReloadSync-101]
_ = x[actDeselect-102] _ = x[actDisableSearch-102]
_ = x[actUnbind-103] _ = x[actEnableSearch-103]
_ = x[actRebind-104] _ = x[actSelect-104]
_ = x[actBecome-105] _ = x[actDeselect-105]
_ = x[actResponse-106] _ = x[actUnbind-106]
_ = x[actShowHeader-107] _ = x[actRebind-107]
_ = x[actHideHeader-108] _ = x[actBecome-108]
_ = x[actResponse-109]
_ = x[actShowHeader-110]
_ = x[actHideHeader-111]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
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, 640, 655, 670, 687, 694, 699, 708, 719, 730, 743, 758, 769, 782, 789, 802, 815, 832, 847, 860, 874, 888, 904, 924, 936, 959, 977, 1001, 1019, 1036, 1046, 1062, 1084, 1097, 1113, 1125, 1139, 1155, 1173, 1193, 1215, 1229, 1244, 1250, 1264, 1279, 1289, 1305, 1320, 1330, 1338, 1345, 1354, 1367, 1383, 1398, 1407, 1418, 1427, 1436, 1445, 1456, 1469, 1482} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1274, 1280, 1294, 1309, 1319, 1335, 1350, 1360, 1368, 1375, 1384, 1397, 1413, 1428, 1437, 1448, 1457, 1466, 1475, 1486, 1499, 1512}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {
+1 -1
View File
@@ -152,7 +152,7 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma // Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1 bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite initialCharClass = charWhite
// A minor optimization that can give 15%+ performance boost // A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass asciiCharClasses [unicode.MaxASCII + 1]charClass
+1 -1
View File
@@ -3,7 +3,7 @@
package algo package algo
var normalized map[rune]rune = map[rune]rune{ var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
+3 -3
View File
@@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) { func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string var remaining string
i := -1 var i int
if delimiter == 0 { if delimiter == 0 {
// Faster than strings.IndexAny(";:") // Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';') i = strings.IndexByte(s, ';')
@@ -312,7 +312,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive // Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error. // integers and does not allocate on error.
code := 0 code := 0
for _, ch := range sbytes(s) { for _, ch := range stringBytes(s) {
ch -= '0' ch -= '0'
if ch > 9 { if ch > 9 {
return -1, delimiter, remaining return -1, delimiter, remaining
@@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0 state256 := 0
ptr := &state.fg ptr := &state.fg
var delimiter byte = 0 var delimiter byte
count := 0 count := 0
for len(ansiCode) != 0 { for len(ansiCode) != 0 {
var num int var num int
+2 -2
View File
@@ -342,8 +342,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
state := interpretCode(code, prevState) state := interpretCode(code, prevState)
if expected != state.ToString() { if expected != state.ToString() {
t.Errorf("expected: %s, actual: %s", t.Errorf("expected: %s, actual: %s",
strings.Replace(expected, "\x1b[", "\\x1b[", -1), strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1)) strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
} }
} }
assert("\x1b[m", nil, "") assert("\x1b[m", nil, "")
+8 -2
View File
@@ -12,8 +12,14 @@ type ChunkCache struct {
} }
// NewChunkCache returns a new ChunkCache // NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache { func NewChunkCache() *ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)} return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
func (cc *ChunkCache) Clear() {
cc.mutex.Lock()
cc.cache = make(map[*Chunk]*queryCache)
cc.mutex.Unlock()
} }
// Add adds the list to the cache // Add adds the list to the cache
+44 -4
View File
@@ -48,7 +48,12 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 { if len(cs) == 0 {
return 0 return 0
} }
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count if len(cs) == 1 {
return cs[0].count
}
// First chunk might not be full due to --tail=N
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
} }
// Push adds the item to the list // Push adds the item to the list
@@ -72,18 +77,53 @@ func (cl *ChunkList) Clear() {
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
cl.mutex.Lock() cl.mutex.Lock()
changed := false
if tail > 0 && CountItems(cl.chunks) > tail {
changed = true
// Find the number of chunks to keep
numChunks := 0
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
numChunks++
left -= cl.chunks[i].count
}
// Copy the chunks to keep
ret := make([]*Chunk, numChunks)
copy(ret, cl.chunks[len(cl.chunks)-numChunks:])
for left, i := tail, len(ret)-1; i >= 0; i-- {
chunk := ret[i]
if chunk.count > left {
newChunk := *chunk
newChunk.count = left
oldCount := chunk.count
for i := 0; i < left; i++ {
newChunk.items[i] = chunk.items[oldCount-left+i]
}
ret[i] = &newChunk
break
}
left -= chunk.count
}
cl.chunks = ret
}
ret := make([]*Chunk, len(cl.chunks)) ret := make([]*Chunk, len(cl.chunks))
copy(ret, cl.chunks) copy(ret, cl.chunks)
// Duplicate the last chunk // Duplicate the first and the last chunk
if cnt := len(ret); cnt > 0 { if cnt := len(ret); cnt > 0 {
if tail > 0 && cnt > 1 {
newChunk := *ret[0]
ret[0] = &newChunk
}
newChunk := *ret[cnt-1] newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk ret[cnt-1] = &newChunk
} }
cl.mutex.Unlock() cl.mutex.Unlock()
return ret, CountItems(ret) return ret, CountItems(ret), changed
} }
+39 -3
View File
@@ -17,7 +17,7 @@ func TestChunkList(t *testing.T) {
}) })
// Snapshot // Snapshot
snapshot, count := cl.Snapshot() snapshot, count, _ := cl.Snapshot(0)
if len(snapshot) > 0 || count > 0 { if len(snapshot) > 0 || count > 0 {
t.Error("Snapshot should be empty now") t.Error("Snapshot should be empty now")
} }
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
} }
// But the new snapshot should contain the added items // But the new snapshot should contain the added items
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 1 && count != 2 { if len(snapshot) != 1 && count != 2 {
t.Error("Snapshot should not be empty now") t.Error("Snapshot should not be empty now")
} }
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
} }
// New snapshot // New snapshot
snapshot, count = cl.Snapshot() snapshot, count, _ = cl.Snapshot(0)
if len(snapshot) != 3 || !snapshot[0].IsFull() || if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk") t.Error("Expected two full chunks and one more chunk")
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items:", lastChunkCount) t.Error("Unexpected number of items:", lastChunkCount)
} }
} }
func TestChunkListTail(t *testing.T) {
cl := NewChunkList(func(item *Item, s []byte) bool {
item.text = util.ToChars(s)
return true
})
total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i)))
}
snapshot, count, changed := cl.Snapshot(0)
assertCount := func(expected int, shouldChange bool) {
if count != expected || CountItems(snapshot) != expected {
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
}
if changed != shouldChange {
t.Error("Unexpected change status")
}
}
assertCount(total, false)
tail := chunkSize + chunkSize/2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, false)
snapshot, count, changed = cl.Snapshot(0)
assertCount(tail, false)
tail = chunkSize / 2
snapshot, count, changed = cl.Snapshot(tail)
assertCount(tail, true)
}
+5 -5
View File
@@ -67,9 +67,9 @@ const (
) )
const ( const (
exitCancel = -1 ExitOk = 0
exitOk = 0 ExitNoMatch = 1
exitNoMatch = 1 ExitError = 2
exitError = 2 ExitBecome = 126
exitInterrupt = 130 ExitInterrupt = 130
) )
+100 -49
View File
@@ -2,10 +2,9 @@
package fzf package fzf
import ( import (
"fmt" "os"
"sync" "sync"
"time" "time"
"unsafe"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -19,30 +18,54 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header) Matcher -> EvtHeader -> Terminal (update header)
*/ */
func ustring(data []byte) string { type revision struct {
return unsafe.String(unsafe.SliceData(data), len(data)) major int
minor int
} }
func sbytes(data string) []byte { func (r *revision) bumpMajor() {
return unsafe.Slice(unsafe.StringData(data), len(data)) r.major++
r.minor = 0
}
func (r *revision) bumpMinor() {
r.minor++
}
func (r revision) equals(other revision) bool {
return r.major == other.major && r.minor == other.minor
}
func (r revision) compatible(other revision) bool {
return r.major == other.major
} }
// Run starts fzf // Run starts fzf
func Run(opts *Options, version string, revision string) { func Run(opts *Options) (int, error) {
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
return runTmux(os.Args, opts)
}
if needWinpty(opts) {
return runWinpty(os.Args, opts)
}
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
defer util.RunAtExitFuncs() defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
}
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
util.Exit(exitOk)
}
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
@@ -56,16 +79,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored { if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState lineAnsiState = newState
return util.ToChars(sbytes(trimmed)), offsets return util.ToChars(stringBytes(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(ustring(data), nil, nil) trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(sbytes(trimmed)), nil return util.ToChars(stringBytes(trimmed)), nil
} }
} }
} }
@@ -77,7 +100,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, ustring(data)) header = append(header, byteString(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
@@ -88,7 +111,7 @@ func Run(opts *Options, version string, revision string) {
}) })
} else { } else {
chunkList = NewChunkList(func(item *Item, data []byte) bool { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(ustring(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
if prevLineAnsiState != nil { if prevLineAnsiState != nil {
@@ -112,7 +135,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return false return false
} }
item.text, item.colors = ansiProcessor(sbytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces() item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
@@ -121,14 +144,17 @@ func Run(opts *Options, version string, revision string) {
}) })
} }
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader var reader *Reader
if !streamingFilter { if !streamingFilter {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
// Matcher // Matcher
@@ -144,14 +170,16 @@ func Run(opts *Options, version string, revision string) {
forward = true forward = true
} }
} }
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
inputRevision := 0 inputRevision := revision{}
snapshotRevision := 0 snapshotRevision := revision{}
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision) matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -178,13 +206,14 @@ func Run(opts *Options, version string, revision string) {
mutex.Unlock() mutex.Unlock()
} }
return false return false
}, eventBox, opts.ReadZero, false) }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot() // NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
@@ -194,9 +223,9 @@ func Run(opts *Options, version string, revision string) {
} }
} }
if found { if found {
util.Exit(exitOk) return ExitOk, nil
} }
util.Exit(exitNoMatch) return ExitNoMatch, nil
} }
// Synchronous search // Synchronous search
@@ -207,9 +236,13 @@ func Run(opts *Options, version string, revision string) {
// Go interactive // Go interactive
go matcher.Loop() go matcher.Loop()
defer matcher.Stop()
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox) terminal, err := NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
maxFit := 0 // Maximum number of items that can fit on screen maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
@@ -226,7 +259,7 @@ func Run(opts *Options, version string, revision string) {
// Event coordination // Event coordination
reading := true reading := true
ticks := 0 ticks := 0
var nextCommand *string var nextCommand *commandSpec
var nextEnviron []string var nextEnviron []string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
total := 0 total := 0
@@ -247,14 +280,17 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false useSnapshot := false
var snapshot []*Chunk var snapshot []*Chunk
var count int var count int
restart := func(command string, environ []string) { restart := func(command commandSpec, environ []string) {
reading = true reading = true
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision++ inputRevision.bumpMajor()
header = make([]string, 0, opts.HeaderLines) header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ) go reader.restart(command, environ)
} }
exitCode := ExitOk
stop := false
for { for {
delay := true delay := true
ticks++ ticks++
@@ -275,7 +311,11 @@ func Run(opts *Options, version string, revision string) {
if reading { if reading {
reader.terminate() reader.terminate()
} }
util.Exit(value.(int)) quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil { if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron) restart(*nextCommand, nextEnviron)
@@ -289,17 +329,20 @@ func Run(opts *Options, version string, revision string) {
useSnapshot = false useSnapshot = false
} }
if !useSnapshot { if !useSnapshot {
if snapshotRevision != inputRevision { if !snapshotRevision.compatible(inputRevision) {
query = []rune{} query = []rune{}
} }
snapshot, count = chunkList.Snapshot() var changed bool
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
snapshotRevision = inputRevision snapshotRevision = inputRevision
} }
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
opts.Sync = false terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
} }
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
@@ -307,7 +350,7 @@ func Run(opts *Options, version string, revision string) {
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision) matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew: case EvtSearchNew:
var command *string var command *commandSpec
var environ []string var environ []string
var changed bool var changed bool
switch val := value.(type) { switch val := value.(type) {
@@ -333,7 +376,10 @@ func Run(opts *Options, version string, revision string) {
break break
} }
if !useSnapshot { if !useSnapshot {
newSnapshot, newCount := chunkList.Snapshot() newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
if changed {
inputRevision.bumpMinor()
}
// We want to avoid showing empty list when reload is triggered // We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed // and the query string is changed at the same time i.e. command != nil && changed
if command == nil || newCount > 0 { if command == nil || newCount > 0 {
@@ -376,20 +422,24 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count == 0 {
util.Exit(exitOk) exitCode = ExitNoMatch
} }
util.Exit(exitNoMatch) stop = true
return
} }
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val) terminal.UpdateList(val, true)
} }
} }
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks)*coordinatorDelayStep,
@@ -397,4 +447,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur) time.Sleep(dur)
} }
} }
return exitCode, err
} }
+35
View File
@@ -0,0 +1,35 @@
package fzf
import (
"os"
"strings"
"unsafe"
)
func WriteTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-temp-*")
if err != nil {
// Unable to create temporary file
// FIXME: Should we terminate the program?
return ""
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
return f.Name()
}
func removeFiles(files []string) {
for _, filename := range files {
os.Remove(filename)
}
}
func stringBytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
func byteString(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}
+3 -1
View File
@@ -1,6 +1,8 @@
package fzf package fzf
import ( import (
"math"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
return item.text.Index return item.text.Index
} }
var minItem = Item{text: util.Chars{Index: -1}} var minItem = Item{text: util.Chars{Index: math.MinInt32}}
func (item *Item) TrimLength() uint16 { func (item *Item) TrimLength() uint16 {
return item.text.TrimLength() return item.text.TrimLength()
+36 -19
View File
@@ -16,11 +16,12 @@ type MatchRequest struct {
pattern *Pattern pattern *Pattern
final bool final bool
sort bool sort bool
revision int revision revision
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
type Matcher struct { type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool tac bool
@@ -29,7 +30,7 @@ type Matcher struct {
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
revision int revision revision
} }
const ( const (
@@ -38,10 +39,11 @@ const (
) )
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher { sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
cache: cache,
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
@@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for { for {
var request MatchRequest var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) { m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events { for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) { switch val := val.(type) {
case MatchRequest: case MatchRequest:
request = val request = val
@@ -71,12 +78,17 @@ func (m *Matcher) Loop() {
} }
events.Clear() events.Clear()
}) })
if stop {
break
}
cacheCleared := false
if request.sort != m.sort || request.revision != m.revision { if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort m.sort = request.sort
m.revision = request.revision m.revision = request.revision
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]*Merger)
clearChunkCache() m.cache.Clear()
cacheCleared = true
} }
// Restart search // Restart search
@@ -85,20 +97,20 @@ func (m *Matcher) Loop() {
cancelled := false cancelled := false
count := CountItems(request.chunks) count := CountItems(request.chunks)
foundCache := false if !cacheCleared {
if count == prevCount { if count == prevCount {
// Look up mergerCache // Look up mergerCache
if cached, found := m.mergerCache[patternString]; found { if cached, found := m.mergerCache[patternString]; found {
foundCache = true merger = cached
merger = cached }
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
} }
} else {
// Invalidate mergerCache
prevCount = count
m.mergerCache = make(map[string]*Merger)
} }
if !foundCache { if merger == nil {
merger, cancelled = m.scan(request) merger, cancelled = m.scan(request)
} }
@@ -150,6 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return PassMerger(&request.chunks, m.tac, request.revision), false return PassMerger(&request.chunks, m.tac, request.revision), false
} }
minIndex := request.chunks[0].items[0].Index()
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -221,11 +234,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision, minIndex), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -236,3 +249,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
} }
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}
+23 -9
View File
@@ -3,8 +3,8 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
func EmptyMerger(revision int) *Merger { func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision) return NewMerger(nil, [][]Result{}, false, false, revision, 0)
} }
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
@@ -20,19 +20,25 @@ type Merger struct {
final bool final bool
count int count int
pass bool pass bool
revision int revision revision
minIndex int32
} }
// PassMerger returns a new Merger that simply returns the items in the // PassMerger returns a new Merger that simply returns the items in the
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32
if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index()
}
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
chunks: chunks, chunks: chunks,
tac: tac, tac: tac,
count: 0, count: 0,
pass: true, pass: true,
revision: revision} revision: revision,
minIndex: minIndex}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
tac: tac, tac: tac,
final: false, final: false,
count: 0, count: 0,
revision: revision} revision: revision,
minIndex: minIndex}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
} }
// Revision returns revision number // Revision returns revision number
func (mg *Merger) Revision() int { func (mg *Merger) Revision() revision {
return mg.revision return mg.revision
} }
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
func (mg *Merger) FindIndex(itemIndex int32) int { func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1 index := -1
if mg.pass { if mg.pass {
index = int(itemIndex) index = int(itemIndex - mg.minIndex)
if mg.tac { if mg.tac {
index = mg.count - index - 1 index = mg.count - index - 1
} }
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
firstChunk := (*mg.chunks)[0]
if firstChunk.count < chunkSize && idx >= firstChunk.count {
idx -= firstChunk.count
chunk := (*mg.chunks)[idx/chunkSize+1]
return Result{item: &chunk.items[idx%chunkSize]}
}
chunk := (*mg.chunks)[idx/chunkSize] chunk := (*mg.chunks)[idx/chunkSize]
return Result{item: &chunk.items[idx%chunkSize]} return Result{item: &chunk.items[idx%chunkSize]}
} }
+8 -7
View File
@@ -23,10 +23,11 @@ func randResult() Result {
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
assert(t, EmptyMerger(0).Length() == 0, "Not empty") r := revision{}
assert(t, EmptyMerger(0).count == 0, "Invalid count") assert(t, EmptyMerger(r).Length() == 0, "Not empty")
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists") assert(t, EmptyMerger(r).count == 0, "Invalid count")
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]Result, []Result) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false, 0) mg := NewMerger(nil, lists, false, false, revision{}, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(nil, lists, true, false, 0) mg := NewMerger(nil, lists, true, false, revision{}, 0)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false, 0) mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))
+1083 -483
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -3,9 +3,11 @@
package fzf package fzf
import "errors"
func (o *Options) initProfiling() error { func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" { if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
errorExit("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling") return errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
} }
return nil return nil
} }
+30 -37
View File
@@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) {
func TestSplitNth(t *testing.T) { func TestSplitNth(t *testing.T) {
{ {
ranges := splitNth("..") ranges, _ := splitNth("..")
if len(ranges) != 1 || if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis || ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis { ranges[0].end != rangeEllipsis {
@@ -88,7 +88,7 @@ func TestSplitNth(t *testing.T) {
} }
} }
{ {
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1") ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
} }
func TestIrrelevantNth(t *testing.T) { func TestIrrelevantNth(t *testing.T) {
index := 0
{ {
opts := defaultOptions() opts := defaultOptions()
words := []string{"--nth", "..", "-x"} words := []string{"--nth", "..", "-x"}
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
@@ -118,7 +119,7 @@ func TestIrrelevantNth(t *testing.T) {
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} { for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %v", opts.Nth) t.Errorf("nth should be empty: %v", opts.Nth)
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
{ {
opts := defaultOptions() opts := defaultOptions()
words = append(words, "-x") words = append(words, "-x")
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if len(opts.Nth) != 2 { if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %v", opts.Nth) t.Errorf("nth should not be empty: %v", opts.Nth)
@@ -137,7 +138,7 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) { checkEvent := func(e tui.Event, s string) {
if pairs[e] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s) t.Errorf("%s != %s", pairs[e], s)
@@ -163,7 +164,7 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE") checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
@@ -177,7 +178,7 @@ func TestParseKeys(t *testing.T) {
check(tui.Left, "left") check(tui.Left, "left")
check(tui.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
@@ -206,40 +207,40 @@ func TestParseKeysWithComma(t *testing.T) {
} }
} }
pairs := parseKeyChords(",", "") pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "") pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "") pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "") pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "") pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c") check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "") pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "") pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,") check(pairs, tui.AltKey(','), "ALT-,")
} }
@@ -262,17 +263,13 @@ func TestBind(t *testing.T) {
} }
} }
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp) check(tui.Key('c'), "", actPageUp)
@@ -290,20 +287,17 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, "f1:abort", errorFn) parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := tui.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark, _ := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
@@ -311,7 +305,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
light := parseTheme(theme, "dark,light") light, _ := parseTheme(theme, "dark,light")
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
@@ -322,7 +316,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
customized := parseTheme(theme, "fg:231,bg:232") customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 { if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
@@ -335,17 +329,18 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
} }
func TestDefaultCtrlNP(t *testing.T) { func TestDefaultCtrlNP(t *testing.T) {
index := 0
check := func(words []string, et tui.EventType, expected actionType) { check := func(words []string, et tui.EventType, expected actionType) {
e := et.AsEvent() e := et.AsEvent()
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[e][0].t != expected { if opts.Keymap[e][0].t != expected {
t.Error() t.Error()
@@ -371,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
} }
func optsFor(words ...string) *Options { func optsFor(words ...string) *Options {
index := 0
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(&index, opts, words)
postProcessOptions(opts) postProcessOptions(opts)
return opts return opts
} }
@@ -475,7 +471,7 @@ func TestValidateSign(t *testing.T) {
} }
func TestParseSingleActionList(t *testing.T) { func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {}) actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
if len(actions) != 4 { if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions)) t.Errorf("Invalid number of actions parsed:%d", len(actions))
} }
@@ -491,11 +487,8 @@ func TestParseSingleActionList(t *testing.T) {
} }
func TestParseSingleActionListError(t *testing.T) { func TestParseSingleActionListError(t *testing.T) {
err := "" _, err := parseSingleActionList("change-query(foobar)baz")
parseSingleActionList("change-query(foobar)baz", func(e string) { if err == nil {
err = e
})
if len(err) == 0 {
t.Errorf("Failed to detect error") t.Errorf("Failed to detect error")
} }
} }
+13 -25
View File
@@ -60,32 +60,17 @@ type Pattern struct {
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]algo.Algo procFun map[termType]algo.Algo
cache *ChunkCache
} }
var ( var _splitRegex *regexp.Regexp
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
func init() { func init() {
_splitRegex = regexp.MustCompile(" +") _splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
}
func clearChunkCache() {
_cache = NewChunkCache()
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
@@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes) asString = string(runes)
} }
cached, found := _patternCache[asString] // We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
cached, found := patternCache[asString]
if found { if found {
return cached return cached
} }
@@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey() ptr.cacheKey = ptr.buildCacheKey()
@@ -162,19 +150,19 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr patternCache[asString] = ptr
return ptr return ptr
} }
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1) str = strings.ReplaceAll(str, "\\ ", "\t")
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
switchSet := false switchSet := false
afterBar := false afterBar := false
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
lowerText := strings.ToLower(text) lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect || caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText caseMode == CaseSmart && text != lowerText
@@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil { if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
// Prefix/suffix cache // Prefix/suffix cache
space := _cache.Search(chunk, cacheKey) space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab) matches := p.matchChunk(chunk, space, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) p.cache.Add(chunk, cacheKey, matches)
} }
return matches return matches
} }
+18 -26
View File
@@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
} }
} }
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
}
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc")) chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
@@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
} }
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str)) chars := util.ToChars([]byte(str))
@@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
} }
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc")) pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg")) pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}}) trans := Transform(tokens, []Range{{1, 1}})
@@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if pat.cacheable != cacheable { if pat.cacheable != cacheable {
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
} }
clearPatternCache()
} }
test(false, "foo !bar", "foo !bar", true) test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true) test(false, "foo | bar !baz", "foo | bar !baz", true)
@@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) { func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) { test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache() pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if cacheable != pat.cacheable { if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
} }
clearPatternCache()
} }
test(true, "foo bar", "foo\tbar", true) test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false) test(true, "foo 'bar", "foo\tbar", false)
+1 -3
View File
@@ -3,6 +3,4 @@
package protector package protector
// Protect calls OS specific protections like pledge on OpenBSD // Protect calls OS specific protections like pledge on OpenBSD
func Protect() { func Protect() {}
return
}
+146
View File
@@ -0,0 +1,146 @@
package fzf
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
const becomeSuffix = ".become"
func escapeSingleQuote(str string) string {
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
}
func fifo(name string) (string, error) {
ns := time.Now().UnixNano()
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
output, err := mkfifo(output, 0600)
if err != nil {
return output, err
}
return output, nil
}
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
output, err := fifo("proxy-output")
if err != nil {
return ExitError, err
}
defer os.Remove(output)
// Take the output
go func() {
withOutputPipe(output, func(outputFile io.ReadCloser) {
if opts.Output == nil {
io.Copy(os.Stdout, outputFile)
} else {
reader := bufio.NewReader(outputFile)
sep := opts.PrintSep[0]
for {
item, err := reader.ReadString(sep)
if err != nil {
break
}
opts.Output <- item
}
}
})
}()
var command string
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
} else {
input, err := fifo("proxy-input")
if err != nil {
return ExitError, err
}
defer os.Remove(input)
go func() {
withInputPipe(input, func(inputFile io.WriteCloser) {
if opts.Input == nil {
io.Copy(inputFile, os.Stdin)
} else {
for item := range opts.Input {
fmt.Fprint(inputFile, item+opts.PrintSep)
}
}
})
}()
if withExports {
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
} else {
// For mintty: cannot directly read named pipe from Go code
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
}
}
// To ensure that the options are processed by a POSIX-compliant shell,
// we need to write the command to a temporary file and execute it with sh.
var exports []string
if withExports {
exports = os.Environ()
for idx, pairStr := range exports {
pair := strings.SplitN(pairStr, "=", 2)
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
}
}
temp := WriteTemporaryFile(append(exports, command), "\n")
defer os.Remove(temp)
cmd := cmdBuilder(temp)
cmd.Stderr = os.Stderr
intChan := make(chan os.Signal, 1)
defer close(intChan)
go func() {
if sig, valid := <-intChan; valid {
cmd.Process.Signal(sig)
}
}()
signal.Notify(intChan, os.Interrupt)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
code := exitError.ExitCode()
if code == ExitBecome {
becomeFile := temp + becomeSuffix
data, err := os.ReadFile(becomeFile)
os.Remove(becomeFile)
if err != nil {
return ExitError, err
}
elems := strings.Split(string(data), "\x00")
if len(elems) < 1 {
return ExitError, errors.New("invalid become command")
}
command := elems[0]
env := []string{}
if len(elems) > 1 {
env = elems[1:]
}
executor := util.NewExecutor(opts.WithShell)
ttyin, err := tui.TtyIn()
if err != nil {
return ExitError, err
}
executor.Become(ttyin, env, command)
}
return code, err
}
}
return ExitOk, nil
}
+38
View File
@@ -0,0 +1,38 @@
//go:build !windows
package fzf
import (
"io"
"os"
"golang.org/x/sys/unix"
)
func sh() (string, error) {
return "sh", nil
}
func mkfifo(path string, mode uint32) (string, error) {
return path, unix.Mkfifo(path, mode)
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
if err != nil {
return err
}
task(outputFile)
outputFile.Close()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
if err != nil {
return err
}
task(inputFile)
inputFile.Close()
return nil
}
+81
View File
@@ -0,0 +1,81 @@
//go:build windows
package fzf
import (
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"sync/atomic"
)
var shPath atomic.Value
func sh() (string, error) {
if cached := shPath.Load(); cached != nil {
return cached.(string), nil
}
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
bytes, err := cmd.Output()
if err != nil {
return "", err
}
sh := strings.TrimSpace(string(bytes))
shPath.Store(sh)
return sh, nil
}
func mkfifo(path string, mode uint32) (string, error) {
m := strconv.FormatUint(uint64(mode), 8)
sh, err := sh()
if err != nil {
return path, err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
if err := cmd.Run(); err != nil {
return path, err
}
return path + ".lnk", nil
}
func withOutputPipe(output string, task func(io.ReadCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
outputFile, err := cmd.StdoutPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(outputFile)
cmd.Wait()
return nil
}
func withInputPipe(input string, task func(io.WriteCloser)) error {
sh, err := sh()
if err != nil {
return err
}
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
inputFile, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
task(inputFile)
inputFile.Close()
cmd.Wait()
return nil
}
+37 -11
View File
@@ -18,20 +18,22 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func([]byte) bool pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
event int32 event int32
finChan chan bool finChan chan bool
mutex sync.Mutex mutex sync.Mutex
exec *exec.Cmd exec *exec.Cmd
execOut io.ReadCloser
command *string command *string
killed bool killed bool
wait bool wait bool
} }
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader { func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
} }
func (r *Reader) startEventPoller() { func (r *Reader) startEventPoller() {
@@ -78,6 +80,7 @@ func (r *Reader) terminate() {
r.mutex.Lock() r.mutex.Lock()
r.killed = true r.killed = true
if r.exec != nil && r.exec.Process != nil { if r.exec != nil && r.exec.Process != nil {
r.execOut.Close()
util.KillCommand(r.exec) util.KillCommand(r.exec)
} else { } else {
os.Stdin.Close() os.Stdin.Close()
@@ -85,18 +88,34 @@ func (r *Reader) terminate() {
r.mutex.Unlock() r.mutex.Unlock()
} }
func (r *Reader) restart(command string, environ []string) { func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady) r.event = int32(EvtReady)
r.startEventPoller() r.startEventPoller()
success := r.readFromCommand(command, environ) success := r.readFromCommand(command.command, environ)
r.fin(success) r.fin(success)
removeFiles(command.tempFiles)
}
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) { func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller() r.startEventPoller()
var success bool var success bool
if util.IsTty() { if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty(os.Stdin) {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores) success = r.readFiles(root, opts, ignores)
@@ -147,7 +166,7 @@ func (r *Reader) feed(src io.Reader) {
} }
// We're not making any progress after 100 tries. Stop. // We're not making any progress after 100 tries. Stop.
if n == 0 && err == nil { if n == 0 {
break break
} }
@@ -242,20 +261,27 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock() r.mutex.Lock()
r.killed = false r.killed = false
r.command = &command r.command = &command
r.exec = util.ExecCommand(command, true) r.exec = r.executor.ExecCommand(command, true)
if environ != nil { if environ != nil {
r.exec.Env = environ r.exec.Env = environ
} }
out, err := r.exec.StdoutPipe()
var err error
r.execOut, err = r.exec.StdoutPipe()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock() r.mutex.Unlock()
return false return false
} }
err = r.exec.Start() err = r.exec.Start()
r.mutex.Unlock()
if err != nil { if err != nil {
r.exec = nil
r.mutex.Unlock()
return false return false
} }
r.feed(out)
r.mutex.Unlock()
r.feed(r.execOut)
return r.exec.Wait() == nil return r.exec.Wait() == nil
} }
+2 -1
View File
@@ -10,9 +10,10 @@ import (
func TestReadFromCommand(t *testing.T) { func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
exec := util.NewExecutor("")
reader := NewReader( reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true }, func(s []byte) bool { strs = append(strs, string(s)); return true },
eb, false, true) eb, exec, false, true)
reader.startEventPoller() reader.startEventPoller()
+5 -3
View File
@@ -15,6 +15,7 @@ type Offset [2]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
color tui.ColorPair color tui.ColorPair
match bool
} }
type Result struct { type Result struct {
@@ -109,7 +110,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
if len(itemColors) == 0 { if len(itemColors) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range matchOffsets { for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch}) offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
} }
return offsets return offsets
} }
@@ -193,12 +194,13 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
} }
} }
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color}) offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
} else { } else {
ansi := itemColors[curr-1] ansi := itemColors[curr-1]
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: ansiToColorPair(ansi, colBase)}) color: ansiToColorPair(ansi, colBase),
match: false})
} }
} }
} }
+11 -16
View File
@@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil return listenAddress{parts[0], port}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) { func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
host := address.host host := address.host
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return port, fmt.Errorf("FZF_API_KEY is required to allow remote access") return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
} }
addrStr := fmt.Sprintf("%s:%d", host, port) addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr) listener, err := net.Listen("tcp", addrStr)
if err != nil { if err != nil {
return port, fmt.Errorf("failed to listen on %s", addrStr) return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
} }
if port == 0 { if port == 0 {
addr := listener.Addr().String() addr := listener.Addr().String()
parts := strings.Split(addr, ":") parts := strings.Split(addr, ":")
if len(parts) < 2 { if len(parts) < 2 {
return port, fmt.Errorf("cannot extract port: %s", addr) return nil, port, fmt.Errorf("cannot extract port: %s", addr)
} }
var err error var err error
port, err = strconv.Atoi(parts[len(parts)-1]) port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil { if err != nil {
return port, err return nil, port, err
} }
} }
@@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept() conn, err := listener.Accept()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { if errors.Is(err, net.ErrClosed) {
break return
} else {
continue
} }
continue
} }
conn.Write([]byte(server.handleHttpRequest(conn))) conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close() conn.Close()
} }
listener.Close()
}() }()
return port, nil return listener, port, nil
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http
@@ -217,12 +215,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
} }
body = body[:contentLength] body = body[:contentLength]
errorMessage := "" actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) { if err != nil {
errorMessage = message return bad(err.Error())
})
if len(errorMessage) > 0 {
return bad(errorMessage)
} }
if len(actions) == 0 { if len(actions) == 0 {
return bad("no action specified") return bad("no action specified")
+793 -328
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -13,7 +13,7 @@ import (
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
return replacePlaceholder(replacePlaceholderParams{ replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template, template: template,
stripAnsi: stripAnsi, stripAnsi: stripAnsi,
delimiter: delimiter, delimiter: delimiter,
@@ -23,7 +23,9 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
allItems: allItems, allItems: allItems,
lastAction: actBackwardDeleteCharEof, lastAction: actBackwardDeleteCharEof,
prompt: "prompt", prompt: "prompt",
executor: util.NewExecutor(""),
}) })
return replaced
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
@@ -244,6 +246,7 @@ func TestQuoteEntry(t *testing.T) {
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`} unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`} windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes var effectiveStyle quotes
exec := util.NewExecutor("")
if util.IsWindows() { if util.IsWindows() {
effectiveStyle = windowsStyle effectiveStyle = windowsStyle
@@ -278,7 +281,7 @@ func TestQuoteEntry(t *testing.T) {
} }
for input, expected := range tests { for input, expected := range tests {
escaped := quoteEntry(input) escaped := exec.QuoteEntry(input)
expected = templateToString(expected, effectiveStyle) expected = templateToString(expected, effectiveStyle)
if escaped != expected { if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
@@ -317,9 +320,9 @@ func TestUnixCommands(t *testing.T) {
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows // purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) { func TestWindowsCommands(t *testing.T) {
if !util.IsWindows() { // XXX Deprecated
t.SkipNow() t.SkipNow()
}
tests := []testCase{ tests := []testCase{
// reference: give{template, query, items}, want{output OR match} // reference: give{template, query, items}, want{output OR match}
-19
View File
@@ -5,26 +5,11 @@ package fzf
import ( import (
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"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)
} }
@@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT) signal.Notify(resizeChan, syscall.SIGCONT)
} }
func quoteEntry(entry string) string {
return "'" + escaper.Replace(entry) + "'"
}
-26
View File
@@ -4,8 +4,6 @@ package fzf
import ( import (
"os" "os"
"regexp"
"strings"
) )
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP // NOOP
} }
func quoteEntry(entry string) string {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
if strings.Contains(shell, "cmd") {
// backslash escaping is done here for applications
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
// caret is the escape character for cmd shell
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}
+57
View File
@@ -0,0 +1,57 @@
package fzf
import (
"os"
"os/exec"
"github.com/junegunn/fzf/src/tui"
)
func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments
fzf := args[0]
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
if opts.BorderShape == tui.BorderUndefined {
args = append(args, "--border")
}
argStr := escapeSingleQuote(fzf)
for _, arg := range args {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-tmux --no-height`
// Get current directory
dir, err := os.Getwd()
if err != nil {
dir = "."
}
// Set tmux options for popup placement
// C Both The centre of the terminal
// R -x The right side of the terminal
// P Both The bottom left of the pane
// M Both The mouse position
// W Both The window position on the status line
// S -y The line above or below the status line
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
switch opts.Tmux.position {
case posUp:
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
case posDown:
tmuxArgs = append(tmuxArgs, "-xC", "-yS")
case posLeft:
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
case posRight:
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
case posCenter:
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
}
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
return runProxy(argStr, func(temp string) *exec.Cmd {
sh, _ := sh()
tmuxArgs = append(tmuxArgs, sh, temp)
return exec.Command("tmux", tmuxArgs...)
}, opts, true)
}
+2 -2
View File
@@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
prefixLength := begin prefixLength := begin
for idx := range tokens { for idx := range tokens {
chars := util.ToChars(sbytes(tokens[idx])) chars := util.ToChars(stringBytes(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)} ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length() prefixLength += chars.Length()
} }
@@ -187,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.ToChars(sbytes(joinTokens(tokens))) chars := util.ToChars(stringBytes(joinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {
+5 -4
View File
@@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, Delimiter{}) tokens := Tokenize(input, Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " { if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
@@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{ {
tokens := Tokenize(input, delimiterRegexp(":")) tokens := Tokenize(input, delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
@@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
} }
func TestTransformIndexOutOfBounds(t *testing.T) { func TestTransformIndexOutOfBounds(t *testing.T) {
Transform([]Token{}, splitNth("1")) s, _ := splitNth("1")
Transform([]Token{}, s)
} }
+2 -2
View File
@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false return false
} }
var DefaultBorderShape BorderShape = BorderRounded var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr { func (a Attr) Merge(b Attr) Attr {
return a | b return a | b
@@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7) StrikeThrough = Attr(1 << 7)
) )
func (r *FullscreenRenderer) Init() {} func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {}
+27 -25
View File
@@ -83,34 +83,36 @@ func _() {
_ = x[Alt-72] _ = x[Alt-72]
_ = x[CtrlAlt-73] _ = x[CtrlAlt-73]
_ = x[Invalid-74] _ = x[Invalid-74]
_ = x[Mouse-75] _ = x[Fatal-75]
_ = x[DoubleClick-76] _ = x[Mouse-76]
_ = x[LeftClick-77] _ = x[DoubleClick-77]
_ = x[RightClick-78] _ = x[LeftClick-78]
_ = x[SLeftClick-79] _ = x[RightClick-79]
_ = x[SRightClick-80] _ = x[SLeftClick-80]
_ = x[ScrollUp-81] _ = x[SRightClick-81]
_ = x[ScrollDown-82] _ = x[ScrollUp-82]
_ = x[SScrollUp-83] _ = x[ScrollDown-83]
_ = x[SScrollDown-84] _ = x[SScrollUp-84]
_ = x[PreviewScrollUp-85] _ = x[SScrollDown-85]
_ = x[PreviewScrollDown-86] _ = x[PreviewScrollUp-86]
_ = x[Resize-87] _ = x[PreviewScrollDown-87]
_ = x[Change-88] _ = x[Resize-88]
_ = x[BackwardEOF-89] _ = x[Change-89]
_ = x[Start-90] _ = x[BackwardEOF-90]
_ = x[Load-91] _ = x[Start-91]
_ = x[Focus-92] _ = x[Load-92]
_ = x[One-93] _ = x[Focus-93]
_ = x[Zero-94] _ = x[One-94]
_ = x[Result-95] _ = x[Zero-95]
_ = x[Jump-96] _ = x[Result-96]
_ = x[JumpCancel-97] _ = x[Jump-97]
_ = x[JumpCancel-98]
_ = x[ClickHeader-99]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 458, 467, 477, 487, 498, 506, 516, 525, 536, 551, 568, 574, 580, 591, 596, 600, 605, 608, 612, 618, 622, 632} var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
func (i EventType) String() string { func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) { if i < 0 || i >= EventType(len(_EventType_index)-1) {
+48 -38
View File
@@ -2,6 +2,7 @@ package tui
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@@ -10,6 +11,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
"golang.org/x/term" "golang.org/x/term"
@@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty" const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R") var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8") r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -71,13 +73,14 @@ func (r *LightRenderer) csi(code string) string {
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h") fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
r.queued.Reset() r.queued.Reset()
} }
} }
// Light renderer // Light renderer
type LightRenderer struct { type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme theme *ColorTheme
mouse bool mouse bool
forceBlack bool forceBlack bool
@@ -85,6 +88,7 @@ type LightRenderer struct {
prevDownTime time.Time prevDownTime time.Time
clicks [][2]int clicks [][2]int
ttyin *os.File ttyin *os.File
ttyout *os.File
buffer []byte buffer []byte
origState *term.State origState *term.State
width int width int
@@ -123,19 +127,25 @@ type LightWindow struct {
bg Color bg Color
} }
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
out, err := openTtyOut()
if err != nil {
out = os.Stderr
}
r := LightRenderer{ r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme, theme: theme,
forceBlack: forceBlack, forceBlack: forceBlack,
mouse: mouse, mouse: mouse,
clearOnExit: clearOnExit, clearOnExit: clearOnExit,
ttyin: openTtyIn(), ttyin: ttyin,
ttyout: out,
yoffset: 0, yoffset: 0,
tabstop: tabstop, tabstop: tabstop,
fullscreen: fullscreen, fullscreen: fullscreen,
upOneLine: false, upOneLine: false,
maxHeightFunc: maxHeightFunc} maxHeightFunc: maxHeightFunc}
return &r return &r, nil
} }
func repeat(r rune, times int) string { func repeat(r rune, times int) string {
@@ -153,11 +163,11 @@ func atoi(s string, defaultValue int) int {
return value return value
} }
func (r *LightRenderer) Init() { func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil { if err := r.initPlatform(); err != nil {
errorExit(err.Error()) return err
} }
r.updateTerminalSize() r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +205,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
return nil
} }
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) { func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,15 +244,16 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue) return atoi(env, defaultValue)
} }
func (r *LightRenderer) getBytes() []byte { func (r *LightRenderer) getBytes() ([]byte, error) {
return r.getBytesInternal(r.buffer, false) bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
} }
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock) c, ok := r.getch(nonblock)
if !nonblock && !ok { if !nonblock && !ok {
r.Close() r.Close()
errorExit("Failed to read " + consoleDevice) return nil, errors.New("failed to read " + consoleDevice)
} }
retries := 0 retries := 0
@@ -272,19 +284,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately. // so terminate fzf immediately.
if len(buffer) > maxInputBuffer { if len(buffer) > maxInputBuffer {
r.Close() r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer)) return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
} }
} }
return buffer return buffer, nil
} }
func (r *LightRenderer) GetChar() Event { func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
r.buffer = r.getBytes() r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
} }
if len(r.buffer) == 0 { if len(r.buffer) == 0 {
panic("Empty buffer") return Event{Fatal, 0, nil}
} }
sz := 1 sz := 1
@@ -315,7 +331,9 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
r.buffer = r.getBytes() if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz) ev = r.escSequence(&sz)
} }
return ev return ev
@@ -738,6 +756,7 @@ func (r *LightRenderer) Close() {
r.flush() r.flush()
r.closePlatform() r.closePlatform()
r.restoreTerminal() r.restoreTerminal()
r.closed.Set(true)
} }
func (r *LightRenderer) Top() int { func (r *LightRenderer) Top() int {
@@ -821,44 +840,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runeWidth(w.border.top) hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top { if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
} }
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom { if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw)) w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
} }
} }
func (w *LightWindow) drawBorderVertical(left, right bool) { func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2 vw := runeWidth(w.border.left)
if !left || !right {
width++
}
color := ColBorder color := ColBorder
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
for y := 0; y < w.height; y++ { for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left { if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left)) w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
} }
w.CPrint(color, repeat(' ', width))
if right { if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.right))
} }
} }
@@ -880,7 +887,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(color, string(w.border.left)) w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2)) w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right)) w.CPrint(color, string(w.border.right))
} }
} }
@@ -1011,7 +1021,7 @@ func (w *LightWindow) Print(text string) {
} }
func cleanse(str string) string { func cleanse(str string) string {
return strings.Replace(str, "\x1b", "", -1) return strings.ReplaceAll(str, "\x1b", "")
} }
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
+22 -10
View File
@@ -3,7 +3,7 @@
package tui package tui
import ( import (
"fmt" "errors"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@@ -45,22 +45,29 @@ func (r *LightRenderer) initPlatform() error {
} }
func (r *LightRenderer) closePlatform() { func (r *LightRenderer) closePlatform() {
// NOOP r.ttyout.Close()
} }
func openTtyIn() *os.File { func openTty(mode int) (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, mode, 0)
if err != nil { if err != nil {
tty := ttyname() tty := ttyname()
if len(tty) > 0 { if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { if in, err := os.OpenFile(tty, mode, 0); err == nil {
return in return in, nil
} }
} }
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) return nil, errors.New("failed to open " + consoleDevice)
util.Exit(2)
} }
return in return in, nil
}
func openTtyIn() (*os.File, error) {
return openTty(syscall.O_RDONLY)
}
func openTtyOut() (*os.File, error) {
return openTty(syscall.O_WRONLY)
} }
func (r *LightRenderer) setupTerminal() { func (r *LightRenderer) setupTerminal() {
@@ -86,9 +93,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) { func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n") r.csi("6n")
r.flush() r.flush()
var err error
bytes := []byte{} bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ { for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0) bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes) offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 { if len(offsets) > 3 {
// Add anything we skipped over to the input buffer // Add anything we skipped over to the input buffer
+7 -3
View File
@@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() { go func() {
fd := int(r.inHandle) fd := int(r.inHandle)
b := make([]byte, 1) b := make([]byte, 1)
for { for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT. // HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput) _ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@@ -91,9 +91,13 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput) windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
} }
func openTtyIn() *os.File { func openTtyIn() (*os.File, error) {
// not used // not used
return nil return nil, nil
}
func openTtyOut() (*os.File, error) {
return os.Stderr, nil
} }
func (r *LightRenderer) setupTerminal() error { func (r *LightRenderer) setupTerminal() error {
+16 -8
View File
@@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
@@ -146,13 +145,13 @@ var (
_initialResize bool = true _initialResize bool = true
) )
func (r *FullscreenRenderer) initScreen() { func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen() s, e := tcell.NewScreen()
if e != nil { if e != nil {
errorExit(e.Error()) return e
} }
if e = s.Init(); e != nil { if e = s.Init(); e != nil {
errorExit(e.Error()) return e
} }
if r.mouse { if r.mouse {
s.EnableMouse() s.EnableMouse()
@@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse() s.DisableMouse()
} }
_screen = s _screen = s
return nil
} }
func (r *FullscreenRenderer) Init() { func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" { if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "") os.Setenv("TERM", "")
} }
encoding.Register()
r.initScreen() if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack) initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
} }
func (r *FullscreenRenderer) Top() int { func (r *FullscreenRenderer) Top() int {
@@ -561,7 +565,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ') if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
w.drawBorder(false) w.drawBorder(false)
} }
+2 -1
View File
@@ -3,6 +3,7 @@
package tui package tui
import ( import (
"os"
"testing" "testing"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
// Test the handling of the tcell keyboard events. // Test the handling of the tcell keyboard events.
func TestGetCharEventKey(t *testing.T) { func TestGetCharEventKey(t *testing.T) {
if util.ToTty() { if util.IsTty(os.Stdout) {
// This test is skipped when output goes to terminal, because it causes // This test is skipped when output goes to terminal, because it causes
// some glitches: // some glitches:
// - output lines may not start at the beginning of a row which makes // - output lines may not start at the beginning of a row which makes
+18 -14
View File
@@ -4,12 +4,19 @@ package tui
import ( import (
"os" "os"
"sync/atomic"
"syscall" "syscall"
) )
var devPrefixes = [...]string{"/dev/pts/", "/dev/"} var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
var tty atomic.Value
func ttyname() string { func ttyname() string {
if cached := tty.Load(); cached != nil {
return cached.(string)
}
var stderr syscall.Stat_t var stderr syscall.Stat_t
if syscall.Fstat(2, &stderr) != nil { if syscall.Fstat(2, &stderr) != nil {
return "" return ""
@@ -27,24 +34,21 @@ func ttyname() string {
continue continue
} }
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
return prefix + file.Name() value := prefix + file.Name()
tty.Store(value)
return value
} }
} }
} }
return "" return ""
} }
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin // TtyIn returns terminal device to read user input
func TtyIn() *os.File { func TtyIn() (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) return openTtyIn()
if err != nil { }
tty := ttyname()
if len(tty) > 0 { // TtyIn returns terminal device to write to
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { func TtyOut() (*os.File, error) {
return in return openTtyOut()
}
}
return os.Stdin
}
return in
} }
+10 -3
View File
@@ -2,13 +2,20 @@
package tui package tui
import "os" import (
"os"
)
func ttyname() string { func ttyname() string {
return "" return ""
} }
// TtyIn on Windows returns os.Stdin // TtyIn on Windows returns os.Stdin
func TtyIn() *os.File { func TtyIn() (*os.File, error) {
return os.Stdin return os.Stdin, nil
}
// TtyIn on Windows returns nil
func TtyOut() (*os.File, error) {
return nil, nil
} }
+52 -19
View File
@@ -1,8 +1,6 @@
package tui package tui
import ( import (
"fmt"
"os"
"strconv" "strconv"
"time" "time"
@@ -104,6 +102,7 @@ const (
CtrlAlt CtrlAlt
Invalid Invalid
Fatal
Mouse Mouse
DoubleClick DoubleClick
@@ -130,6 +129,7 @@ const (
Result Result
Jump Jump
JumpCancel JumpCancel
ClickHeader
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {
@@ -303,6 +303,9 @@ type ColorTheme struct {
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
PreviewFg ColorAttr PreviewFg ColorAttr
PreviewBg ColorAttr PreviewBg ColorAttr
DarkBg ColorAttr DarkBg ColorAttr
@@ -314,7 +317,7 @@ type ColorTheme struct {
Spinner ColorAttr Spinner ColorAttr
Info ColorAttr Info ColorAttr
Cursor ColorAttr Cursor ColorAttr
Selected ColorAttr Marker ColorAttr
Header ColorAttr Header ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
@@ -353,7 +356,8 @@ type MouseEvent struct {
type BorderShape int type BorderShape int
const ( const (
BorderNone BorderShape = iota BorderUndefined BorderShape = iota
BorderNone
BorderRounded BorderRounded
BorderSharp BorderSharp
BorderBold BorderBold
@@ -368,6 +372,14 @@ const (
BorderRight BorderRight
) )
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
}
func (s BorderShape) HasRight() bool { func (s BorderShape) HasRight() bool {
switch s { switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
@@ -516,7 +528,7 @@ type TermSize struct {
} }
type Renderer interface { type Renderer interface {
Init() Init() error
Resize(maxHeightFunc func(int) int) Resize(maxHeightFunc func(int) int)
Pause(clear bool) Pause(clear bool)
Resume(clear bool, sigcont bool) Resume(clear bool, sigcont bool)
@@ -595,12 +607,14 @@ var (
ColMatch ColorPair ColMatch ColorPair
ColCursor ColorPair ColCursor ColorPair
ColCursorEmpty ColorPair ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair ColCurrent ColorPair
ColCurrentMatch ColorPair ColCurrentMatch ColorPair
ColCurrentCursor ColorPair ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair ColSpinner ColorPair
ColInfo ColorPair ColInfo ColorPair
@@ -622,6 +636,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined}, Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined}, Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined}, Bg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined}, DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined}, Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined}, Match: ColorAttr{colUndefined, AttrUndefined},
@@ -630,7 +647,7 @@ func EmptyTheme() *ColorTheme {
Spinner: ColorAttr{colUndefined, AttrUndefined}, Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined}, Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined}, Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined}, Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined}, Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: ColorAttr{colUndefined, AttrUndefined},
@@ -652,6 +669,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined}, DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined}, Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline}, Match: ColorAttr{colDefault, Underline},
@@ -660,7 +680,7 @@ func NoColorTheme() *ColorTheme {
Spinner: ColorAttr{colDefault, AttrUndefined}, Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined}, Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined}, Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined}, Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined}, Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined}, Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined}, BorderLabel: ColorAttr{colDefault, AttrUndefined},
@@ -676,17 +696,15 @@ func NoColorTheme() *ColorTheme {
} }
} }
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
util.Exit(2)
}
func init() { func init() {
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined}, DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
@@ -695,7 +713,7 @@ func init() {
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, BorderLabel: ColorAttr{colWhite, AttrUndefined},
@@ -714,6 +732,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
@@ -722,7 +743,7 @@ func init() {
Spinner: ColorAttr{148, AttrUndefined}, Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined}, Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
@@ -741,6 +762,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined}, Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
@@ -749,7 +773,7 @@ func init() {
Spinner: ColorAttr{65, AttrUndefined}, Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined}, Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
@@ -791,12 +815,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner) theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info) theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor) theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header) theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border) theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes // These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled) theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter) theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
@@ -822,17 +849,23 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg) ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg) ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg) ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg) ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg) ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter) ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter) ColCursorEmpty = pair(blank, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter) if theme.SelectedBg.Color != theme.Bg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
}
ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg) ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg) ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg) ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg) ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg) ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg) ColInfo = pair(theme.Info, theme.Bg)
+1 -11
View File
@@ -1,7 +1,6 @@
package util package util
import ( import (
"os"
"sync" "sync"
) )
@@ -25,14 +24,5 @@ func RunAtExitFuncs() {
for i := len(fns) - 1; i >= 0; i-- { for i := len(fns) - 1; i >= 0; i-- {
fns[i]() fns[i]()
} }
} atExitFuncs = nil
// Exit executes any functions registered with AtExit() then exits the program
// with os.Exit(code).
//
// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
// the program before any of the AtExit functions can run.
func Exit(code int) {
defer os.Exit(code)
RunAtExitFuncs()
} }
+30
View File
@@ -1,6 +1,7 @@
package util package util
import ( import (
"bytes"
"fmt" "fmt"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
return chars.slice return chars.slice
} }
func (chars *Chars) NumLines(atMost int) (int, bool) {
lines := 1
if runes := chars.optionalRunes(); runes != nil {
for _, r := range runes {
if r == '\n' {
lines++
}
if lines > atMost {
return atMost, true
}
}
return lines, false
}
for idx := 0; idx < len(chars.slice); idx++ {
found := bytes.IndexByte(chars.slice[idx:], '\n')
if found < 0 {
break
}
idx += found
lines++
if lines > atMost {
return atMost, true
}
}
return lines, false
}
func (chars *Chars) optionalRunes() []rune { func (chars *Chars) optionalRunes() []rune {
if chars.inBytes { if chars.inBytes {
return nil return nil
+36 -8
View File
@@ -3,6 +3,7 @@ package util
import ( import (
"math" "math"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@@ -137,14 +138,10 @@ func DurWithin(
return val return val
} }
// IsTty returns true if stdin is a terminal // IsTty returns true if the file is a terminal
func IsTty() bool { func IsTty(file *os.File) bool {
return isatty.IsTerminal(os.Stdin.Fd()) fd := file.Fd()
} return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
// ToTty returns true if stdout is a terminal
func ToTty() bool {
return isatty.IsTerminal(os.Stdout.Fd())
} }
// Once returns a function that returns the specified boolean value only once // Once returns a function that returns the specified boolean value only once
@@ -188,3 +185,34 @@ func ToKebabCase(s string) string {
} }
return strings.ToLower(name) return strings.ToLower(name)
} }
// CompareVersions compares two version strings
func CompareVersions(v1, v2 string) int {
parts1 := strings.Split(v1, ".")
parts2 := strings.Split(v2, ".")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
}
return n
}
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
var p1, p2 int
if i < len(parts1) {
p1 = atoi(parts1[i])
}
if i < len(parts2) {
p2 = atoi(parts2[i])
}
if p1 > p2 {
return 1
} else if p1 < p2 {
return -1
}
}
return 0
}
+31
View File
@@ -203,3 +203,34 @@ func TestStringWidth(t *testing.T) {
t.Errorf("Expected: %d, Actual: %d", 1, w) t.Errorf("Expected: %d, Actual: %d", 1, w)
} }
} }
func TestCompareVersions(t *testing.T) {
assert := func(a, b string, expected int) {
if result := CompareVersions(a, b); result != expected {
t.Errorf("Expected: %d, Actual: %d", expected, result)
}
}
assert("2", "1", 1)
assert("2", "2", 0)
assert("2", "10", -1)
assert("2.1", "2.2", -1)
assert("2.1", "2.1.1", -1)
assert("1.2.3", "1.2.2", 1)
assert("1.2.3", "1.2.3", 0)
assert("1.2.3", "1.2.3.0", 0)
assert("1.2.3", "1.2.4", -1)
// Different number of parts
assert("1.0.0", "1", 0)
assert("1.0.0", "1.0", 0)
assert("1.0.0", "1.0.0", 0)
assert("1.0", "1.0.0", 0)
assert("1", "1.0.0", 0)
assert("1.0.0", "1.0.0.1", -1)
assert("1.0.0.1.0", "1.0.0.1", 0)
assert("", "3.4.5", -1)
}
+50 -10
View File
@@ -3,31 +3,71 @@
package util package util
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL type Executor struct {
func ExecCommand(command string, setpgid bool) *exec.Cmd { shell string
shell := os.Getenv("SHELL") args []string
if len(shell) == 0 { escaper *strings.Replacer
shell = "sh"
}
return ExecCommandWith(shell, command, setpgid)
} }
// ExecCommandWith executes the given command with the specified shell func NewExecutor(withShell string) *Executor {
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { shell := os.Getenv("SHELL")
cmd := exec.Command(shell, "-c", command) args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
args = args[1:]
} else {
if len(shell) == 0 {
shell = "sh"
}
args = []string{"-c"}
}
var escaper *strings.Replacer
tokens := strings.Split(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("'", "'\\''")
}
return &Executor{shell, args, escaper}
}
// ExecCommand executes the given command with $SHELL
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(x.shell, append(x.args, command)...)
if setpgid { if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
} }
return cmd return cmd
} }
func (x *Executor) QuoteEntry(entry string) string {
return "'" + x.escaper.Replace(entry) + "'"
}
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
shellPath, err := exec.LookPath(x.shell)
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
os.Exit(127)
}
args := append([]string{shellPath}, append(x.args, command)...)
SetStdin(stdin)
syscall.Exec(shellPath, args, environ)
}
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+132 -30
View File
@@ -6,62 +6,164 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
) )
var shellPath atomic.Value type shellType int
const (
shellTypeUnknown shellType = iota
shellTypeCmd
shellTypePowerShell
)
var escapeRegex = regexp.MustCompile(`[&|<>()^%!"]`)
type Executor struct {
shell string
shellType shellType
args []string
shellPath atomic.Value
}
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
} else if len(shell) == 0 {
shell = "cmd"
}
shellType := shellTypeUnknown
basename := filepath.Base(shell)
if len(args) > 0 {
args = args[1:]
} else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd
args = []string{"/s/c"}
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
shellType = shellTypePowerShell
args = []string{"-NoProfile", "-Command"}
} else {
args = []string{"-c"}
}
return &Executor{shell: shell, shellType: shellType, args: args}
}
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd { // FIXME: setpgid is unused. We set it in the Unix implementation so that we
var shell string // can kill preview process with its child processes at once.
if cached := shellPath.Load(); cached != nil { // NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := x.shell
if cached := x.shellPath.Load(); cached != nil {
shell = cached.(string) shell = cached.(string)
} else { } else {
shell = os.Getenv("SHELL") if strings.Contains(shell, "/") {
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output() out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil { if err == nil {
shell = strings.Trim(string(out), "\n") shell = strings.Trim(string(out), "\n")
} }
} }
shellPath.Store(shell) x.shellPath.Store(shell)
} }
return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with the specified shell
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
var cmd *exec.Cmd var cmd *exec.Cmd
if strings.Contains(shell, "cmd") { if x.shellType == shellTypeCmd {
cmd = exec.Command(shell) cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false, HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command), CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: 0, CreationFlags: 0,
} }
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else { } else {
cmd = exec.Command(shell, "-c", command) cmd = exec.Command(shell, append(x.args, command)...)
} cmd.SysProcAttr = &syscall.SysProcAttr{
cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: false,
HideWindow: false, CreationFlags: 0,
CreationFlags: 0, }
} }
return cmd return cmd
} }
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
cmd := x.ExecCommand(command, false)
cmd.Stdin = stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = environ
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
os.Exit(127)
}
err = cmd.Wait()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
}
os.Exit(0)
}
func escapeArg(s string) string {
b := make([]byte, 0, len(s)+2)
b = append(b, '"')
slashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
default:
slashes = 0
case '\\':
slashes++
case '"':
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '\\')
}
b = append(b, c)
}
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '"')
return escapeRegex.ReplaceAllStringFunc(string(b), func(match string) string {
return "^" + match
})
}
func (x *Executor) QuoteEntry(entry string) string {
switch x.shellType {
case shellTypeCmd:
/* Manually tested with the following commands:
fzf --preview "echo {}"
fzf --preview "type {}"
echo .git\refs\| fzf --preview "dir {}"
echo .git\refs\\| fzf --preview "dir {}"
echo .git\refs\\\| fzf --preview "dir {}"
reg query HKCU | fzf --reverse --bind "enter:reload(reg query {})"
fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!"
fd -H --no-ignore -td -d 4 | fzf --preview "dir {}"
fd -H --no-ignore -td -d 4 | fzf --preview "eza {}" --preview-window up
fd -H --no-ignore -td -d 4 | fzf --preview "eza --color=always --tree --level=3 --icons=always {}"
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
*/
return escapeArg(entry)
case shellTypePowerShell:
escaped := strings.ReplaceAll(entry, `"`, `\"`)
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
default:
return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
}
}
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return cmd.Process.Kill() return cmd.Process.Kill()
+13
View File
@@ -0,0 +1,13 @@
//go:build !windows
package fzf
import "errors"
func needWinpty(_ *Options) bool {
return false
}
func runWinpty(_ []string, _ *Options) (int, error) {
return ExitError, errors.New("Not supported")
}
+75
View File
@@ -0,0 +1,75 @@
//go:build windows
package fzf
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/junegunn/fzf/src/util"
)
func isMintty345() bool {
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
}
func needWinpty(opts *Options) bool {
if os.Getenv("TERM_PROGRAM") != "mintty" {
return false
}
if isMintty345() {
/*
See: https://github.com/junegunn/fzf/issues/3809
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
*/
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
return false
}
// Setting the environment variable here unfortunately doesn't help,
// so we need to start a child process with "MSYS=enable_pcon"
// os.Setenv("MSYS", "enable_pcon")
return true
}
if opts.NoWinpty {
return false
}
if _, err := exec.LookPath("winpty"); err != nil {
return false
}
return true
}
func runWinpty(args []string, opts *Options) (int, error) {
sh, err := sh()
if err != nil {
return ExitError, err
}
argStr := escapeSingleQuote(args[0])
for _, arg := range args[1:] {
argStr += " " + escapeSingleQuote(arg)
}
argStr += ` --no-winpty`
if isMintty345() {
return runProxy(argStr, func(temp string) *exec.Cmd {
cmd := exec.Command(sh, temp)
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}, opts, false)
}
return runProxy(argStr, func(temp string) *exec.Cmd {
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}, opts, false)
}
+258 -158
View File
File diff suppressed because it is too large Load Diff