m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-09 03:43:49 -05:00

Compare commits

...

182 Commits

Author SHA1 Message Date
dependabot[bot]
76dcfe85f2 Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 13:32:21 +00:00
Junegunn Choi
8cdfb23df6 0.66.0 2025-10-12 22:17:52 +09:00
Junegunn Choi
4ffde48e2f Fix --bold inheritance
Fix #4548
2025-10-12 13:58:46 +09:00
Junegunn Choi
f2b33f038a Revert "Make query string in --disabled state bold as before"
This reverts commit ab407c4645.
2025-10-12 13:58:46 +09:00
junegunn
d5913bf86e Deploying to master from @ junegunn/fzf@0e9026b817 🚀 2025-10-12 00:02:08 +00:00
Jacobo de Vera
0e9026b817 feat: Allow disabling Ctrl-R binding in shell integration (#4535)
Close #4417
2025-10-12 01:57:31 +09:00
Junegunn Choi
ab407c4645 Make query string in --disabled state bold as before
Fix #4546
2025-10-11 09:35:48 +09:00
Junegunn Choi
91c4bef35f Update CHANGELOG 2025-10-10 03:41:37 +09:00
Junegunn Choi
bf77206221 Improve Unix domain socket handling
- Check if the file is in use
- Change the permission to 0600
2025-10-09 13:52:10 +09:00
Junegunn Choi
0cb1be3f04 Fix --help output: socket path cannot be omitted 2025-10-09 01:12:30 +09:00
Junegunn Choi
01cb38a5fb Add Unix domain socket support for --listen
Close #4541
2025-10-09 01:07:59 +09:00
Junegunn Choi
c38c6cad79 Update CHANGELOG 2025-10-09 00:17:00 +09:00
Junegunn Choi
ba6fc40cfd Add 'best' to man page 2025-10-09 00:17:00 +09:00
Junegunn Choi
dd46a256c0 Fix offset-up and offset-down with --layout=reverse-list
Related: 3df06a1c68
2025-10-09 00:17:00 +09:00
Junegunn Choi
d19ce0ad8d Add 'best' action 2025-10-09 00:17:00 +09:00
Junegunn Choi
ed7becfb47 Go to the closest match when disabling raw mode 2025-10-09 00:17:00 +09:00
Junegunn Choi
9ace1351ff ADD $FZF_DIRECTION 2025-10-09 00:17:00 +09:00
Junegunn Choi
e1de29bc40 CTRL-R: Bind ALT-R to toggle-raw 2025-10-09 00:17:00 +09:00
Junegunn Choi
0df7d10550 Rename: '--color hidden' to '--color nomatch' 2025-10-09 00:17:00 +09:00
Junegunn Choi
91e119a77e Fix non-matching items not refreshing after clearing query 2025-10-09 00:17:00 +09:00
Junegunn Choi
3984161f6c Fix: 'hidden' style not applied to text without colors 2025-10-09 00:17:00 +09:00
Junegunn Choi
91beacf0f4 Add special 'strip' style attribute for stripping colors
Test cases:
  fd --color always | fzf --ansi --delimiter /
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim,nth:regular
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strikethrough --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strip:strikethrough --raw
  fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular,hidden:strip:dim:strikethrough --raw
2025-10-09 00:17:00 +09:00
Junegunn Choi
e6ad01fb90 Revise color configuration 2025-10-09 00:17:00 +09:00
Junegunn Choi
ce2200e908 Do not allow gutter characters with width other than 1 2025-10-09 00:17:00 +09:00
Junegunn Choi
548061dbde --gutter ' ' --color gutter:reverse 2025-10-09 00:17:00 +09:00
Junegunn Choi
8f0c91545d Add $FZF_RAW for conditional actions 2025-10-09 00:17:00 +09:00
Junegunn Choi
0eefcf348e Update CHANGELOG 2025-10-09 00:17:00 +09:00
Junegunn Choi
c1f8d18a0c Add enable-raw and disable-raw actions 2025-10-09 00:17:00 +09:00
Junegunn Choi
8585969d6d Refactor action implementation 2025-10-09 00:17:00 +09:00
Junegunn Choi
8a943a9b1a Remove TODO comments 2025-10-09 00:17:00 +09:00
Junegunn Choi
c87a8eccd4 Add '--bind ctrl-x:toggle-raw' to CTRL-R bindings 2025-10-09 00:17:00 +09:00
Junegunn Choi
65df0abf0e Introduce 'raw' mode 2025-10-09 00:17:00 +09:00
junegunn
b51bc6b50e Deploying to master from @ junegunn/fzf@febaadbee5 🚀 2025-10-05 00:02:16 +00:00
Junegunn Choi
febaadbee5 Fix stray character artifacts when scrollbar is hidden
Fix #4537
2025-10-04 21:56:56 +09:00
dependabot[bot]
0e67c5aa7a Bump actions/setup-go from 5 to 6 (#4513)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-29 21:20:26 +09:00
mickychang9
760d1b7c58 refactor: use maps.Copy and maps.Clone (#4518)
Signed-off-by: mickychang9 <mickychang9@outlook.com>
2025-09-29 18:11:19 +09:00
Junegunn Choi
9bdacc8df2 Update --help output to avoid confusion 2025-09-28 23:56:51 +09:00
junegunn
8e936ecfa7 Deploying to master from @ junegunn/fzf@db2e95b1f2 🚀 2025-09-28 00:02:15 +00:00
Junegunn Choi
db2e95b1f2 Remove unused field 2025-09-27 22:34:12 +09:00
alex-huff
687074e772 merger: fix chunk cache never getting cleared (#4531)
Commit 7fc13c5 indroduced less aggressive cache invalidation for the
chunk cache but saved the new revision before comparing it with the old
one, and so the cache was never considered invalid.

Fixes #4529
2025-09-27 09:01:13 +09:00
Junegunn Choi
3401c2e0c7 Remove alignment in .tool-versions for RuboCop 2025-09-24 22:41:54 +09:00
Junegunn Choi
e8cb315419 Apply shfmt to bash script files (make fmt) 2025-09-24 22:41:54 +09:00
Junegunn Choi
f0c4ee4047 make lint: Perform bash script linting 2025-09-24 22:41:54 +09:00
xieyonn
de0df2422a feat: add make fmt for *.sh *.bash
1. add .editorconfig file, add rules for .sh .bash files.
2. add make fmt target, use:
    - gofmt *.go.
    - shfmt *.sh *.bash, shell/completion.bash, shell/key-bindings.bash
        need a left indent due to an outermost if block.
3. add shfmt check for bash scripts in make lint target.
4. install shfmt in actions.
2025-09-24 22:41:54 +09:00
Massimo Mund
148b0a94cd tui/light: consume full 7-byte CSI sequences to prevent leftover printing (#4528)
- Fix parsing in escSequence so 7-byte CSI forms (e.g. ESC [ 5 ; 10 ~) set *sz = 7 and the entire sequence is consumed.
- Prevents trailing bytes (like 10~) from remaining in the input buffer and being printed as stray characters.
2025-09-23 23:33:41 +09:00
Junegunn Choi
ca294109c3 Apply RuboCop suggestions 2025-09-22 21:23:54 +09:00
Junegunn Choi
9cad2686e9 Update .tool-versions 2025-09-21 21:19:49 +09:00
junegunn
9a45172232 Deploying to master from @ junegunn/fzf@2a92c7d792 🚀 2025-09-21 00:02:16 +00:00
Junegunn Choi
2a92c7d792 Adjust base16 (16) theme (#4501)
Motivation:

`--color base16` can be a better default than `dark` or `light`, since it uses
the colors defined by the current theme. This usually blends in more
naturally and works well in both light and dark modes.

However, some elements were previously hard-coded with white or black
foreground colors, which can cause rendering issues in certain terminal
themes.
2025-09-17 19:38:49 +09:00
Junegunn Choi
f5975cf870 Add --gutter to --help and man page 2025-09-16 21:30:01 +09:00
Junegunn Choi
a67aa85820 Style change: thinner gutter column (#4521) 2025-09-16 21:22:56 +09:00
dependabot[bot]
c5cabe1691 Bump github.com/charlievieth/fastwalk from 1.0.13 to 1.0.14 (#4522)
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.13 to 1.0.14.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.13...v1.0.14)

---
updated-dependencies:
- dependency-name: github.com/charlievieth/fastwalk
  dependency-version: 1.0.14
  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>
2025-09-16 01:19:14 +09:00
Junegunn Choi
cbed41cd82 No emoji 2025-09-14 21:08:48 +09:00
Junegunn Choi
6684771cbf Fix CTRL-Z for tcell renderer by using the official API
See https://github.com/gdamore/tcell/pull/431
2025-09-14 11:41:12 +09:00
Junegunn Choi
f5f894ea47 Fix rendering of multiple OSC 8 links in a single line
Fix #4517
2025-09-14 11:26:47 +09:00
junegunn
a0a334fc8d Deploying to master from @ junegunn/fzf@ae12e94b1f 🚀 2025-09-07 00:02:03 +00:00
Massimo Mund
ae12e94b1f Add sub-word actions (#3997)
Add `backward-subword`, `forward-subword`, `kill-subword`, `backward-kill-subword` actions.
2025-09-05 19:38:22 +09:00
Massimo Mund
9ed971cc90 Add keybindings for CTRL, ALT, SHIFT + UP, DOWN, RIGHT, LEFT, HOME, END, BACKSPACE, DELETE & more (#3996)
* Added tests for `LightRenderer`

* Added common SHIFT, ALT and ALT+SHIFT key sequences

* Added common CTRL key sequences

* Added common CTRL+ALT, CTRL+SHIFT, CTRL+ALT+SHIFT key sequences

* Added proper xterm META modifier handling

according to defc6dd568/input.c (L357-L375)

* Fix `ctrl-backspace` and `ctrl-alt-backspace`

* Fix broken tcell tests on windows by swallowing Resize events

* Added tests for  FullscreenRenderer

* Removed own fork of tcell and updated tcell to 2.9.0

tcell 2.9.0 is needed for `Ctrl-Alt-*` and `Ctrl-Alt-Shift-*` shortcuts in Windows

* Replace conditional checks with switch statements to improve readability

* Replace long conditionals with constant slices to improve readability

* Bind `ctrl-bspace` (`ctrl-h`) to `backward-delete-char` by default

Since we now distinguish between Backspace and Ctrl-Backspace, Ctrl-Backspace should trigger the same action as Backspace by default. In that way nothing changes for the user but you can bind other actions to Ctrl-Backspace when desired.
2025-09-05 14:56:51 +09:00
Junegunn Choi
129cb23078 Require Go 1.23 2025-09-05 14:45:17 +09:00
dependabot[bot]
d22812e917 Bump github.com/gdamore/tcell/v2 from 2.8.1 to 2.9.0 (#4503)
Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.8.1 to 2.9.0.
- [Release notes](https://github.com/gdamore/tcell/releases)
- [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv2.md)
- [Commits](https://github.com/gdamore/tcell/compare/v2.8.1...v2.9.0)

---
updated-dependencies:
- dependency-name: github.com/gdamore/tcell/v2
  dependency-version: 2.9.0
  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>
2025-09-05 14:39:37 +09:00
Charlie Vieth
10d712824a mod: update charlievieth/fastwalk to v1.0.13 and min Go version to 1.21 (#4508)
This commit updates github.com/charlievieth/fastwalk to v1.0.13 which
addresses fastwalk issue #61. It also updates the minimum supported Go
version to 1.21 (up from 1.20) since that is now the minimum version
supported by fastwalk.
2025-09-04 22:04:52 +09:00
Junegunn Choi
de4059c8fa Update README 2025-09-03 08:18:57 +09:00
Junegunn Choi
416aff86e9 0.65.2 2025-08-31 22:18:44 +09:00
zhedazijingang
59dc7f178f refactor: replace []byte(fmt.Sprintf) with fmt.Appendf (#4507)
Signed-off-by: zhedazijingang <unwrap_or_else@outlook.com>
2025-08-31 22:01:35 +09:00
junegunn
a3c9f8bfee Deploying to master from @ junegunn/fzf@5546c65491 🚀 2025-08-31 00:02:03 +00:00
Junegunn Choi
5546c65491 Fix rendering of items with tabs when using a non-default ellipsis
Fix #4505
2025-08-27 23:31:31 +09:00
junegunn
f2179f015c Deploying to master from @ junegunn/fzf@9a53d84b9c 🚀 2025-08-24 00:02:30 +00:00
Junegunn Choi
9a53d84b9c Update README.md 2025-08-22 22:51:04 +09:00
Junegunn Choi
0a8ff7899c Do not unset FZF_DEFAULT_* variables when using winpty
Fix #4497
Fix #4400
2025-08-22 19:24:01 +09:00
xty
f9d7877d8b [bash 3] Fix CTRL-T and ALT-C to preserve the last yank (#4496) 2025-08-19 23:31:02 +09:00
Peter Sideris
9fe9976591 Fix a typo in man page (#4495) 2025-08-19 23:25:57 +09:00
Chayoung You
de1824f71d [install] Support old uname in macOS (#4492) 2025-08-17 11:54:32 +09:00
dependabot[bot]
19a9296c47 Bump actions/checkout from 4 to 5 (#4485)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 21:17:29 +09:00
Ioannis Pinakoulakis
49967f3d45 Use fixed-length array when possible (#4488) 2025-08-15 21:16:41 +09:00
longhutianjie
978b6254c7 chore: remove redundant word in comment (#4490)
Signed-off-by: longhutianjie <keplrnewton@icloud.com>
2025-08-14 13:26:29 +09:00
Junegunn Choi
1afd143810 Fix incorrect truncation of --info-command with --info=inline-right
Fix #4479
2025-08-08 18:51:24 +09:00
Junegunn Choi
e5cd7f0a3a 0.65.1 2025-08-03 14:41:56 +09:00
junegunn
51d3940c63 Deploying to master from @ junegunn/fzf@179aec1578 🚀 2025-08-03 00:02:30 +00:00
Junegunn Choi
179aec1578 Fix '--color nth:regular' not to reset ANSI attributes of the original text 2025-08-03 00:54:26 +09:00
Junegunn Choi
af0014aba8 Fix a bug where you cannot unset the default --nth using change-nth 2025-08-03 00:29:05 +09:00
Junegunn Choi
da3d995709 Fix $FZF_CLICK_{HEADER,FOOTER}_WORD with ANSI colors and tabs 2025-08-02 16:47:09 +09:00
Junegunn Choi
04c4269db3 0.65.0 2025-07-27 10:39:41 +09:00
junegunn
78f238294f Deploying to master from @ junegunn/fzf@354d0468c1 🚀 2025-07-27 00:02:23 +00:00
LangLangBart
354d0468c1 fix(shell): check for mawk existence before version check (#4468)
close #4463
2025-07-25 17:33:18 +09:00
Junegunn Choi
4efcc344c3 Add 'trigger(KEY_OR_EVENT[,...])' action 2025-07-23 19:41:06 +09:00
Junegunn Choi
5818b58350 Better fix for #4465 - remove unnecessary erase 2025-07-23 19:30:52 +09:00
Junegunn Choi
7941129cc4 Add 'click-footer' event 2025-07-22 23:24:23 +09:00
Junegunn Choi
069d71a840 Fix rendering error when hiding a preview window without border
This was a regression introduced in cdcab267.

Fix #4465
2025-07-22 19:23:10 +09:00
Junegunn Choi
08027e7a79 Fix --no-header-lines-border behavior
It should be different from --header-lines-border=none according to the
man page. It should merge two headers unlike the latter.
2025-07-22 19:16:55 +09:00
Junegunn Choi
ead302981c Add support for {*n} and {*nf} placeholder
Close #4458
2025-07-20 10:53:58 +09:00
junegunn
fe0ffa14ff Deploying to master from @ junegunn/fzf@821b8e70a8 🚀 2025-07-20 00:02:23 +00:00
Junegunn Choi
821b8e70a8 [neovim] Fix margin background color when &winborder is used
Fix #4453
2025-07-19 16:19:48 +09:00
Jaseem Abid
8ceda54c7d Fix a typo in README.md (#4459) 2025-07-16 23:19:43 +09:00
junegunn
84e515bd6e Deploying to master from @ junegunn/fzf@dea1df6878 🚀 2025-07-13 00:02:29 +00:00
Junegunn Choi
dea1df6878 Add missing mention of 'bg-cancel' to the man page 2025-07-12 20:09:54 +09:00
Junegunn Choi
0076ec2e8d 0.64.0 2025-07-06 22:11:36 +09:00
Junegunn Choi
82c9671f79 Fix selection lost on revision bump 2025-07-06 22:02:12 +09:00
Junegunn Choi
d364a1122e Fix regression where header is not updated 2025-07-06 20:24:23 +09:00
Junegunn Choi
fb570e94e7 Update: make generate 2025-07-06 20:03:13 +09:00
Junegunn Choi
6e3c830cd2 Add 'multi' event triggered on multi-selection changes 2025-07-06 10:05:25 +09:00
junegunn
d7db7fc132 Deploying to master from @ junegunn/fzf@ff1550bb38 🚀 2025-07-06 00:02:27 +00:00
Junegunn Choi
ff1550bb38 Normalize halfwidth and fullwidth characers for matching 2025-07-03 20:57:19 +09:00
Junegunn Choi
976001e474 Explain the need to escape placeholders in transform actions 2025-07-02 22:26:56 +09:00
Junegunn Choi
531dd6fb4f Update copyright year 2025-07-02 22:10:05 +09:00
Junegunn Choi
ba035f2a76 Run preview command when preview window appears after CTRL-Z
80b8846318
2025-07-02 21:40:02 +09:00
Junegunn Choi
d34675d3c9 Fix panic caused by incorrect update ordering
Fix #4442

Make sure to prepare windows before rendering elements.

Thanks to @nugged for the report.
2025-07-02 21:28:11 +09:00
junegunn
ce95adc66c Deploying to master from @ junegunn/fzf@397fe8e395 🚀 2025-06-29 00:02:28 +00:00
Junegunn Choi
397fe8e395 0.63.0 2025-06-28 01:11:00 +09:00
Junegunn Choi
111266d832 Reset full-background property after a new line 2025-06-27 23:21:48 +09:00
dependabot[bot]
19d858f9b6 Bump github.com/charlievieth/fastwalk from 1.0.10 to 1.0.12 (#4431)
Bumps [github.com/charlievieth/fastwalk](https://github.com/charlievieth/fastwalk) from 1.0.10 to 1.0.12.
- [Release notes](https://github.com/charlievieth/fastwalk/releases)
- [Commits](https://github.com/charlievieth/fastwalk/compare/v1.0.10...v1.0.12)

---
updated-dependencies:
- dependency-name: github.com/charlievieth/fastwalk
  dependency-version: 1.0.12
  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>
2025-06-27 15:47:41 +09:00
Junegunn Choi
79690724d8 Fix exact boundary match with --scheme=path or --tiebreak end
Fix #4438
2025-06-26 22:33:58 +09:00
Junegunn Choi
5ed87ffcb9 Fix highlight offsets of multi-line entries
Fix regression from 4811e52a
2025-06-26 20:48:34 +09:00
Junegunn Choi
b99cb6323f Update CHANGELOG 2025-06-25 08:34:06 +09:00
Junegunn Choi
debf3d8a8a Refactor ANSI parser 2025-06-25 08:26:14 +09:00
Junegunn Choi
4811e52af3 Support full-line background color in the list section
Close #4432
2025-06-25 02:12:10 +09:00
Junegunn Choi
8d81730ec2 with-nth: Do not trim trailing whitespaces with background colors
Example:
  echo -en '  \e[48;5;232mhello\e[48;5;147m  ' | fzf --ansi --with-nth 1
2025-06-24 20:27:24 +09:00
Junegunn Choi
330a85c25c Allow \e[K in addition to \e[0K for full-line background 2025-06-23 22:12:32 +09:00
Junegunn Choi
3a21116307 Terminate running background transform on exit (addendum)
Close #4422
2025-06-22 01:53:25 +09:00
Junegunn Choi
247d168af6 Terminate running background transform on exit
Close #4422
2025-06-21 23:24:38 +09:00
Junegunn Choi
b2a8a283c7 Reorganize code to ensure deletion of temp files 2025-06-21 23:06:46 +09:00
Junegunn Choi
c36ddce36f Add bg-cancel action to ignore running background transforms
Close #4430

Example:

  # Implement popup that disappears after 1 second
  #   * Use footer as the popup
  #   * Use `bell` to ring the terminal bell
  #   * Use `bg-transform-footer` to clear the footer after 1 second
  #   * Use `bg-cancel` to ignore currently running background transform actions
  fzf --multi --list-border \
      --bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
      --bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
      --bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
2025-06-21 17:28:48 +09:00
Junegunn Choi
c35d9cff7d Avoid full redraw when changing header and footer windows 2025-06-21 12:40:56 +09:00
Junegunn Choi
549ce3cf6c Do not reserve a single column at the end when scrollbar is hidden
Close #4410

Example:
    fzf --pointer '' --marker '' --no-scrollbar --wrap --wrap-sign ''
2025-06-20 08:22:58 +09:00
Junegunn Choi
575bc0768c Update .goreleaser to build android_arm64 binary (#4428) 2025-06-19 22:57:16 +09:00
Junegunn Choi
89334e881e Update man page and changelog 2025-06-19 22:56:41 +09:00
Junegunn Choi
dcec6354f5 Add {*} placeholder flag 2025-06-19 22:35:23 +09:00
Junegunn Choi
16d338da84 Revert "Add {*} placeholder flag"
This reverts commit 27258f7207.
2025-06-19 12:39:31 +09:00
Junegunn Choi
27258f7207 Add {*} placeholder flag 2025-06-19 01:04:59 +09:00
曹家巧
4d2d6a5ced chore: fix function name (#4425)
Signed-off-by: xiaoxiangirl <caojiaqiao@outlook.com>
2025-06-19 00:47:14 +09:00
Junegunn Choi
0c00b203e6 Implement asynchronous transform actions (#4419)
Close #4418

Example:

    fzf --bind 'focus:bg-transform-header(sleep 2; date; echo {})'
2025-06-16 00:39:11 +09:00
Junegunn Choi
3b68dcdd81 Add footer
Options:
  --footer=STR             String to print as footer
  --footer-border[=STYLE]  Draw border around the footer section
                           [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
                            top|bottom|left|right|line|none] (default: line)
  --footer-label=LABEL     Label to print on the footer border
  --footer-label-pos=COL   Position of the footer label
                           [POSITIVE_INTEGER: columns from left|
                            NEGATIVE_INTEGER: columns from right][:bottom]
                           (default: 0 or center)

The default border type for footer is 'line', which draws a single
separator between the footer and the list. It changes its position
depending on `--layout`, so you don't have to manually switch between
'top' and 'bottom'

The 'line' style is now supported by other border types as well.
`--list-border` is the only exception.
2025-06-10 23:02:23 +09:00
Junegunn Choi
39db026161 Fix inconsistent placement of header-lines with border options
fzf displayed --header-lines inconsistently depending on the presence of borders:

  # --header and --header-lines co-located
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first

  # --header and --header-lines separated
  seq 10 | fzf --header-lines 3 --header "$(seq 101 103)" --header-first --header-lines-border

This commit fixes the inconsistency with the following logic:

* If only one of --header or --header-lines is provided, --header-first
  applies to that single header.
* If both are present, --header-first affects only the regular --header,
  not --header-lines.
2025-06-10 23:02:23 +09:00
Koichi Murase
f6c589c606 [bash,zsh] Skip comments in ~/.ssh/config
For the line "Host host1 # this is a comment", the current
implementation generates words in an inline comment as hostnames.
This patch removes the comment before generating the hostname.
2025-06-09 21:46:53 +09:00
Koichi Murase
2bd29c3172 [bash,zsh] Support "=" after "Hostname" and "Host" in ~/.ssh/config
In ~/.ssh/config, "=" can also be used as a separator between the
field name and the value.  The current master does not properly handle
this and generate a hostname "=" or one starting with "=".  This patch
correctly handles it.
2025-06-09 21:46:53 +09:00
Koichi Murase
4a61f53b85 [bash,zsh] Remove redundant filtering-out of comment/blank lines
Comments are anyway removed in the subsequent call to `sub(/#.*/,
"")`, and it becomes a blank line.  Blank lines do not have fields, so
they are ignored in the next for-loop.
2025-06-09 21:46:53 +09:00
Koichi Murase
adc9ad28da [bash,zsh] Correctly exclude the hostname "0.0.0.0"
In the current implementation, any hostnames in /etc/hosts containing
"0.0.0.0" as a part (such as "110.0.0.0" would be excluded.  "0.0.0.0"
should be checked by the exact match.
2025-06-09 21:46:53 +09:00
Koichi Murase
585cfaef8b [bash,zsh] Do not end the hostname analysis on "]" in ~/.ssh/known_hosts
An entry of the form `[example.com]:port,192.168.0.1 ...` in
~/.ssh/known_hosts are not properly processed.  The current
implementation gives up the matching on the first occurrence of `]`,
the subsequent 192.168.0.1 would not be extracted.  This patch
continues the analysis and removes "]" together with "[".

This patch also removes the ":port" part from the hostnames in
~/.ssh/known_hosts.  One cannot use the form "hostname:port" in the
arguments to the ssh command anyway.
2025-06-09 21:46:53 +09:00
Koichi Murase
b5cd8880b1 [bash,zsh] Process hostnames with uppercase letters in known_hosts 2025-06-09 21:46:53 +09:00
junegunn
44ddab881e Deploying to master from @ junegunn/fzf@bfa287b66d 🚀 2025-06-08 00:02:27 +00:00
Koichi Murase
bfa287b66d [bash,zsh] Separate common functions into "shell/common.sh" 2025-06-08 00:00:17 +09:00
Koichi Murase
243e52fa11 [bash,zsh] Work around mawk 1.3.3-20090705 not supporting the POSIX brackets 2025-06-08 00:00:17 +09:00
Koichi Murase
c166eaba6d [bash,zsh] Work around Solaris awk, which is non-standard
Solaris awk at /usr/bin/awk is meant for backward compatibility with
an ancient implementation of 1977 awk in the original UNIX.  It lacks
many features of POSIX awk.  To use a standard-conforming version in
Solaris, one needs to explicitly use /usr/xpg4/bin/awk.
2025-06-08 00:00:17 +09:00
Koichi Murase
09194c24f2 [bash,zsh] Work around a quirk of macOS awk
macOS awk is a variant of nawk, but it contains a unique patch for the
UTF-8 support.  However, this patch causes the problem.  If the input
contains any non-UTF-8 data, macOS awk stops processing and does not
do anything, instead of ignoring the unrecognized data and continue
the processing.  However, the contents of the ssh configuration and
/etc/hosts is not under the control of fzf, so we cannot fix the input
when those files contain non-UTF-8 data.  To work around this
behavior, one can set the locale to LC_ALL=C to treat the input data
with the plain 8-bit encoding.
2025-06-08 00:00:17 +09:00
Koichi Murase
ec521e47aa [bash,zsh] Reduce the number of fork & exec 2025-06-05 13:02:11 +09:00
Koichi Murase
e3f4a51c18 [zsh] Set shell options for pathname expansion "~/.ssh/config.d/*"
This applies the same changes as commit 0a06fd6f for Bash (GitHub PR
2025-06-05 13:02:11 +09:00
Koichi Murase
0a06fd6f63 [bash] Set shell options for pathname expansion "~/.ssh/config.d/*" (#4405) 2025-06-03 20:47:28 +09:00
Koichi Murase
70eace5290 Fix the CI failure for PR caused by a spelling mistake (#4406) 2025-06-03 19:41:47 +09:00
junegunn
40f9f254a9 Deploying to master from @ junegunn/fzf@15d6c17390 🚀 2025-06-01 00:02:34 +00:00
Junegunn Choi
15d6c17390 Fix ANSI attributes lost when nth:regular is set
Example:
  # foo was not displayed in italic
  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color fg:dim,nth:regular --nth 1
2025-05-30 21:02:35 +09:00
Junegunn Choi
a9d1d42436 Fix ANSI attributes lost when 'regular' attribute is set to fg or nth
Examples:

  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color fg:regular
  echo -e "\x1b[33;3mfoo \x1b[mbar" | fzf --ansi --color nth:regular
2025-05-30 20:43:20 +09:00
Junegunn Choi
1ecfa38eee [bash] Fix 'complete' errors when IFS is newline
Fix #4342
2025-05-30 20:41:50 +09:00
Junegunn Choi
54fd92b7dd --no-color: Keep ANSI attributes in the list
Example:
  echo -e "\x1b[33;3mfoo \x1b[34;4mbar\x1b[m baz" | fzf --ansi --no-color
2025-05-30 20:33:21 +09:00
Junegunn Choi
835906d392 --no-color: Keep ANSI attributes in preview window
Example:
  fzf --preview 'echo -e "\x1b[33;3mfoo \x1b[34;4mbar\x1b[m baz"' --no-color
2025-05-30 20:26:53 +09:00
Junegunn Choi
1721e6a1ed Do not apply 'nth' attributes to trailing whitespaces
# foo  bar
    # -----    <- previously underlined trailing whitespace
    # ---      <- with the fix, trailing whitespace is excluded
    fzf --color nth:underline --nth 1 <<< 'foo  bar'
2025-05-30 19:43:10 +09:00
Junegunn Choi
c7ee3b833f Fix FZF_CLICK_HEADER_NTH for multi-line headers 2025-05-30 17:10:26 +09:00
Junegunn Choi
ffb6e28ca7 Allow customizing --ghost color via '--color ghost'
Examples:

  # Dimmed red
  fzf --ghost booya --color ghost:red

  # Regular red
  fzf --ghost booya --color ghost:red:regular

Close #4398
2025-05-28 00:27:33 +09:00
Junegunn Choi
a4c6846851 Fix background color of 'disabled' query
fzf --color disabled:red,list-bg:blue --disabled --query foo --input-border
2025-05-28 00:17:41 +09:00
Junegunn Choi
d18c0bf694 [man] Add GET endpoint example 2025-05-22 21:57:04 +09:00
Junegunn Choi
4e3f9854e6 Update README.md 2025-05-17 22:06:06 +09:00
Junegunn Choi
b27943423e Show ellipsis for truncated labels
Close #4390
2025-05-17 11:25:15 +09:00
Junegunn Choi
894a1016bc RuboCop lint 2025-05-17 11:20:29 +09:00
Junegunn Choi
efe6cddd34 Update README 2025-05-16 22:15:38 +09:00
Junegunn Choi
f1c6bdf3e8 Update README 2025-05-16 22:15:06 +09:00
Junegunn Choi
710659bcf5 Update SECURITY.md 2025-05-14 11:06:15 +09:00
Josef Andersson
be67775da4 Add initial security policy (#4379)
Signed-off-by: Josef Andersson <janderssonse@proton.me>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 11:05:20 +09:00
jiz4oh
2c6381499c [neovim] Respect winborder of Neovim 0.11+ (#4389)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-05-14 00:53:26 +09:00
junegunn
4df842e78c Deploying to master from @ junegunn/fzf@b81696fb64 🚀 2025-05-11 00:02:09 +00:00
Ajeet D'Souza
b81696fb64 bash: set keybinding right before printing special character (#4377) 2025-05-10 15:29:27 +09:00
Junegunn Choi
d226d841a1 0.62.0 2025-05-04 18:31:18 +09:00
Junegunn Choi
c6d83047e5 Allow whitespace as separator in --color option 2025-05-04 15:08:23 +09:00
Junegunn Choi
46dabccdf1 [vim] Update g:fzf_colors example with 'query' 2025-05-04 14:52:22 +09:00
Junegunn Choi
cd9517b679 Add 'alt-bg' color for striped lines (#4370)
Test cases:

1. 'jump' should show alternating background colors even when 'alt-bg' is
not defined as before.

  go run main.go --bind load:jump

Two differences:
  * The alternating lines will not be in bold (was a bug)
  * The marker column will not be rendered with alternating background color

2. Use alternating background color when 'alt-bg' is set

  go run main.go --color bg:238,alt-bg:237
  go run main.go --color bg:238,alt-bg:237 --highlight-line

3. 'selected-bg' should take precedence

  go run main.go --color bg:238,alt-bg:237,selected-bg:232 \
                 --highlight-line --multi --bind 'load:select+up+select+up'

4. Should work with text with ANSI colors

  declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
    bat --plain --language bash --color always |
    go run main.go --read0 --ansi --reverse --multi \
                   --color bg:237,alt-bg:238,current-bg:236 --highlight-line

---

Close #4354
Fix #4372
2025-05-04 14:32:06 +09:00
junegunn
cd6677ba1d Deploying to master from @ junegunn/fzf@9c1a47acf7 🚀 2025-05-04 00:02:25 +00:00
bitraid
9c1a47acf7 [fish] Support deleting history items with SHIFT-DEL
Bind to SHIFT-DELETE a command that deletes the selected history items.
It can be overridden by $FZF_CTRL_R_OPTS.
2025-04-28 00:27:51 +09:00
bitraid
0c280a3ce1 [fish] Simplify commandline call in fzf-file-widget 2025-04-28 00:27:51 +09:00
bitraid
53e8b6e705 [fish] Add version check 2025-04-28 00:27:51 +09:00
bitraid
ad33165fa7 [fish] History: Operate only on line at cursor
This allows inserting history entries when constructing multiline
commands.
2025-04-28 00:27:51 +09:00
junegunn
2055db61c8 Deploying to master from @ junegunn/fzf@d2c662e54f 🚀 2025-04-27 00:02:22 +00:00
Junegunn Choi
d2c662e54f Reset coordinator delay on 'reload'
Fix #4364
2025-04-25 21:30:25 +09:00
Junegunn Choi
d24b58ef3f 0.61.3 2025-04-22 20:53:23 +09:00
RafaelDominiquini
06ae9b0f3b Add missing environment variables (#4356)
Co-authored-by: Rafael Baboni Dominiquini <rafaeldominiquini@gmail.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-04-22 20:51:43 +09:00
Junegunn Choi
2a9c1c06a4 Revert "Disable tmux popup when already running inside one (#4351)"
This reverts commit af8fe918d8.

Fix #4360
Fix #4359
2025-04-22 20:20:21 +09:00
76 changed files with 5835 additions and 1570 deletions

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
root = true
[*.{sh,bash}]
indent_style = space
indent_size = 2
simplify = true
binary_next_line = false
switch_case_indent = true
space_redirects = true
function_next_line = false
# also bash scripts.
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
indent_style = space
indent_size = 2
simplify = true
binary_next_line = false
switch_case_indent = true
space_redirects = true
function_next_line = false

View File

@@ -27,18 +27,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4

View File

@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v4 uses: actions/dependency-review-action@v4

View File

@@ -1,5 +1,5 @@
--- ---
name: Test fzf on Linux name: build
on: on:
push: push:
@@ -18,22 +18,22 @@ jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "1.20" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.1 ruby-version: 3.4.6
- name: Install packages - name: Install packages
run: sudo apt-get install --yes zsh fish tmux run: sudo apt-get install --yes zsh fish tmux shfmt
- name: Install Ruby gems - name: Install Ruby gems
run: bundle install run: bundle install

View File

@@ -15,14 +15,14 @@ jobs:
build: build:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "1.20" go-version: "1.23"
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
@@ -30,7 +30,7 @@ jobs:
ruby-version: 3.0.0 ruby-version: 3.0.0
- name: Install packages - name: Install packages
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
- name: Install Ruby gems - name: Install Ruby gems
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1 run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout 🛎️ - name: Checkout 🛎️
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Generate Sponsors 💖 - name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1 uses: JamesIves/github-sponsors-readme-action@v1

View File

@@ -6,5 +6,5 @@ jobs:
name: Spell Check with Typos name: Spell Check with Typos
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- uses: crate-ci/typos@v1.29.4 - uses: crate-ci/typos@v1.29.4

View File

@@ -14,6 +14,7 @@ builds:
- windows - windows
- freebsd - freebsd
- openbsd - openbsd
- android
goarch: goarch:
- amd64 - amd64
- arm - arm
@@ -22,9 +23,9 @@ builds:
- ppc64le - ppc64le
- s390x - s390x
goarm: goarm:
- 5 - "5"
- 6 - "6"
- 7 - "7"
flags: flags:
- -trimpath - -trimpath
ldflags: ldflags:
@@ -38,6 +39,10 @@ builds:
goarch: arm64 goarch: arm64
- goos: openbsd - goos: openbsd
goarch: arm64 goarch: arm64
- goos: android
goarch: amd64
- goos: android
goarch: arm
# .goreleaser.yaml # .goreleaser.yaml
notarize: notarize:

View File

@@ -1,2 +1,3 @@
golang 1.20.13 golang 1.23
ruby 3.4.1 ruby 3.4
shfmt 3.12

View File

@@ -6,7 +6,7 @@ Build instructions
### Prerequisites ### Prerequisites
- Go 1.20 or above - Go 1.23 or above
### Using Makefile ### Using Makefile
@@ -41,6 +41,20 @@ make release
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof > --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
> ``` > ```
Running tests
-------------
```sh
# Run go unit tests
make test
# Run integration tests (requires to be on tmux)
make itest
# Run a single test case
ruby test/runner.rb --name test_something
```
Third-party libraries used Third-party libraries used
-------------------------- --------------------------

View File

@@ -1,6 +1,479 @@
CHANGELOG CHANGELOG
========= =========
0.66.0
------
### Quick summary
This version introduces many new features centered around the new "raw" mode.
| Type | Class | Name | Description |
| :-- | :-- | :-- | :-- |
| New | Option | `--raw` | Enable raw mode by default |
| New | Option | `--gutter CHAR` | Set the gutter column character |
| New | Option | `--gutter-raw CHAR` | Set the gutter column character in raw mode |
| Enhancement | Option | `--listen SOCKET` | Added support for Unix domain sockets |
| New | Action | `toggle-raw` | Toggle raw mode |
| New | Action | `enable-raw` | Enable raw mode |
| New | Action | `disable-raw` | Disable raw mode |
| New | Action | `up-match` | Move up to the matching item |
| New | Action | `down-match` | Move down to the matching item |
| New | Action | `best` | Move to the matching item with the best score |
| New | Color | `nomatch` | Color for non-matching items in raw mode |
| New | Env Var | `FZF_RAW` | Matching status in raw mode (0, 1, or undefined) |
| New | Env Var | `FZF_DIRECTION` | `up` or `down` depending on the layout |
| New | Env Var | `FZF_SOCK` | Path to the Unix domain socket fzf is listening on |
| Enhancement | Key | `CTRL-N` | `down` -> `down-match` |
| Enhancement | Key | `CTRL-P` | `up` -> `up-match` |
| Enhancement | Shell | `CTRL-R` binding | Toggle raw mode with `ALT-R` |
| Enhancement | Shell | `CTRL-R` binding | Opt-out with an empty `FZF_CTRL_R_COMMAND` |
### 1. Introducing "raw" mode
![](https://github.com/user-attachments/assets/9640ae11-b5f7-43fb-95f1-c29307fc17c2)
This version introduces a new "raw" mode (named so because it shows the list
"unfiltered"). In raw mode, non-matching items stay in their original positions,
but appear dimmed. This allows you see surrounding items of a match and better
understand the context of it. You can enable raw mode by default with `--raw`,
but it's often more useful when toggled dynamically with the `toggle-raw`
action.
```sh
tree | fzf --reverse --bind alt-r:toggle-raw
```
While non-matching items are displayed in a dimmed color, they are treated just
like matching items, so you place the cursor on them and perform any action. If
you prefer to navigate only through matching items, use the `down-match` and
`up-match` actions, which are from now on bound to `CTRL-N` and `CTRL-P`
respectively, and also to `ALT-DOWN` and `ALT-UP`.
| Key | Action | With `--history` |
| :-- | :-- | :-- |
| `down` | `down` | |
| `up` | `up` | |
| `ctrl-j` | `down` | |
| `ctrl-k` | `up` | |
| `ctrl-n` | `down-match` | `next-history` |
| `ctrl-p` | `up-match` | `prev-history` |
| `alt-down` | `down-match` | |
| `alt-up` | `up-match` | |
> [!NOTE]
> `CTRL-N` and `CTRL-P` are bound to `next-history` and `prev-history` when
> `--history` option is enabled, so in that case, you'll need to manually bind
> them, or use `ALT-DOWN` and `ALT-UP` instead.
> [!TIP]
> `up-match` and `down-match` are equivalent to `up` and `down` when not in
> raw mode, so you can safely bind them to `up` and `arrow` keys if you prefer.
> ```sh
> fzf --bind up:up-match,down:down-match
> ```
#### Customizing the behavior
In raw mode, the input list is presented in its original order, unfiltered, and
your cursor will not move to the matching item automatically. Here are ways to
customize the behavior.
```sh
# When the result list is updated, move the cursor to the item with the best score
# (assuming sorting is not disabled)
fzf --raw --bind result:best
# Move to the first matching item in the original list
# - $FZF_RAW is set to 0 when raw mode is enabled and the current item is a non-match
# - $FZF_DIRECTION is set to either 'up' or 'down' depending on the layout direction
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
```
#### Customizing the look
##### Gutter
To make the mode visually distinct, the gutter column is rendered in a dashed
line using `▖` character. But you can customize it with the `--gutter-raw CHAR`
option.
```sh
# Use a thinner gutter instead of the default dashed line
fzf --bind alt-r:toggle-raw --gutter-raw ▎
```
##### Color and style of non-matching items
Non-matching items are displayed in a dimmed color by default, but you can
change it with the `--color nomatch:...` option.
```sh
fzf --raw --color nomatch:red
fzf --raw --color nomatch:red:dim
fzf --raw --color nomatch:red:dim:strikethrough
fzf --raw --color nomatch:red:dim:strikethrough:italic
```
For colored input, dimming alone may not be enough, and you may prefer to remove
colors entirely. For that case, a new special style attribute `strip` has been
added.
```sh
fd --color always | fzf --ansi --raw --color nomatch:dim:strip:strikethrough
```
#### Conditional actions for raw mode
You may want to perform different actions depending on whether the current item
is a match or not. For that, fzf now exports `$FZF_RAW` environment variable.
It's:
- Undefined if raw mode is disabled
- `1` if the current item is a match
- `0` otherwise
```sh
# Do not allow selecting non-matching items
fzf --raw --bind 'enter:transform:[[ ${FZF_RAW-1} = 1 ]] && echo accept || echo bell'
```
#### Leveraging raw mode in shell integration
The `CTRL-R` binding (command history) now lets you toggle raw mode with `ALT-R`.
### 2. Style changes
The screenshot on the right shows the updated gutter style:
![](https://github.com/user-attachments/assets/8ea7b5ef-c99e-4686-905b-22eb078b700a)
This version includes a few minor updates to fzf's classic visual style:
- The gutter column is now narrower, rendered with the left-half block character (`▌`).
- Markers no longer use background colors.
- The `--color base16` theme (alias: `16`) has been updated for better compatibility with both dark and light themes.
### 3. `--listen` now supports Unix domain sockets
If an argument to `--listen` ends with `.sock`, fzf will listen on a Unix
domain socket at the specified path.
```sh
fzf --listen /tmp/fzf.sock --no-tmux
# GET
curl --unix-socket /tmp/fzf.sock http
# POST
curl --unix-socket /tmp/fzf.sock http -d up
```
Note that any existing file at the given path will be removed before creating
the socket, so avoid using an important file path.
### 4. Added options
#### `--gutter CHAR`
The gutter column can now be customized using `--gutter CHAR` and styled with
`--color gutter:...`. Examples:
```sh
# Right-aligned gutter
fzf --gutter '▐'
# Even thinner gutter
fzf --gutter '▎'
# Yellow checker pattern
fzf --gutter '▚' --color gutter:yellow
# Classic style
fzf --gutter ' ' --color gutter:reverse
```
#### `--gutter-raw CHAR`
As noted above, the `--gutter-raw CHAR` option was also added for customizing the gutter column in raw mode.
### 5. Added actions
The following actions were introduced to support working with raw mode:
| Action | Description |
| :-- | :-- |
| `toggle-raw` | Toggle raw mode |
| `enable-raw` | Enable raw mode |
| `disable-raw` | Disable raw mode |
| `up-match` | Move up to the matching item; identical to `up` if raw mode is disabled |
| `down-match` | Move down to the matching item; identical to `down` if raw mode is disabled |
| `best` | Move to the matching item with the best score; identical to `first` if raw mode is disabled |
### 6. Added environment variables
#### `$FZF_DIRECTION`
`$FZF_DIRECTION` is now exported to child processes, indicating the list direction of the current layout:
- `up` for the default layout
- `down` for `reverse` or `reverse-list`
This simplifies writing transform actions involving layout-dependent actions
like `{up,down}-match`, `{up,down}-selected`, and `toggle+{up,down}`.
```sh
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
```
#### `$FZF_SOCK`
When fzf is listening on a Unix domain socket using `--listen`, the path to the
socket is exported as `$FZF_SOCK`, analogous to `$FZF_PORT` for TCP sockets.
#### `$FZF_RAW`
As described above, `$FZF_RAW` is now exported to child processes in raw mode,
indicating whether the current item is a match (`1`) or not (`0`). It is not
defined when not in raw mode.
#### `$FZF_CTRL_R_COMMAND`
You can opt-out `CTRL-R` binding from the shell integration by setting
`FZF_CTRL_R_COMMAND` to an empty string. Setting it to any other value is not
supported and will result in a warning.
```sh
# Disable the CTRL-R binding from the shell integration
FZF_CTRL_R_COMMAND= eval "$(fzf --bash)"
```
### 7. Added key support for `--bind`
Pull request [#3996](https://github.com/junegunn/fzf/pull/3996) added support
for many additional keys for `--bind` option, such as `ctrl-backspace`.
### 8. Breaking changes
#### Hiding the gutter column
In the previous versions, the recommended way to hide the gutter column was to
set `--color gutter:-1`. That's because the gutter column was just a space
character, reversed. But now that it's using a visible character (`▌`), applying
the default color is no longer enough to hide it. Instead, you can set it to
a space character.
```sh
# Hide the gutter column
fzf --gutter ' '
# Classic style
fzf --gutter ' ' --color gutter:reverse
```
#### `--color` option
In the previous versions, some elements had default style attributes applied and
you would have to explicitly unset them with `regular` attribute if you wanted
to reset them. This is no longer needed now, as the default style attributes
are applied only when you do not specify any color or style for that element.
```sh
# No 'dim', just red and italic.
fzf --ghost 'Type to search' --color ghost:red:italic
```
#### Compatibility changes
Starting with this release, fzf is built with Go 1.23. Support for some old OS versions has been dropped.
See https://go.dev/wiki/MinimumRequirements.
0.65.2
------
- Bug fixes and improvements
- Fix incorrect truncation of `--info-command` with `--info=inline-right` (#4479)
- [install] Support old uname in macOS (#4492)
- [bash 3] Fix `CTRL-T` and `ALT-C` to preserve the last yank (#4496)
- Do not unset `FZF_DEFAULT_*` variables when using winpty (#4497) (#4400)
- Fix rendering of items with tabs when using a non-default ellipsis (#4505)
- **This is the final release to support Windows 7.**
- Future versions will be built with the latest Go toolchain, which has dropped support for Windows 7.
0.65.1
------
- Fixed incorrect `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_FOOTER_WORD` when the header or footer contains ANSI escape sequences and tab characters.
- Fixed a bug where you cannot unset the default `--nth` using `change-nth` action.
- Fixed a highlighting bug when using `--color fg:dim,nth:regular` pattern over ANSI-colored items.
0.65.0
------
- Added `click-footer` event that is triggered when the footer section is clicked. When the event is triggered, the following environment variables are set:
- `$FZF_CLICK_FOOTER_COLUMN` - clicked column (1-based)
- `$FZF_CLICK_FOOTER_LINE` - clicked line (1-based)
- `$FZF_CLICK_FOOTER_WORD` - the word under the cursor
```sh
fzf --footer $'[Edit] [View]\n[Copy to clipboard]' \
--with-shell 'bash -c' \
--bind 'click-footer:transform:
[[ $FZF_CLICK_FOOTER_WORD =~ Edit ]] && echo "execute:vim \{}"
[[ $FZF_CLICK_FOOTER_WORD =~ View ]] && echo "execute:view \{}"
(( FZF_CLICK_FOOTER_LINE == 2 )) && (( FZF_CLICK_FOOTER_COLUMN < 20 )) &&
echo "execute-silent(echo -n \{} | pbcopy)+bell"
'
```
- Added `trigger(...)` action that triggers events bound to another key or event.
```sh
# You can click on each key name to trigger the actions bound to that key
fzf --footer 'Ctrl-E: Edit / Ctrl-V: View / Ctrl-Y: Copy to clipboard' \
--with-shell 'bash -c' \
--bind 'ctrl-e:execute:vim {}' \
--bind 'ctrl-v:execute:view {}' \
--bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell' \
--bind 'click-footer:transform:
[[ $FZF_CLICK_FOOTER_WORD =~ Ctrl ]] && echo "trigger(${FZF_CLICK_FOOTER_WORD%:})"
'
```
- You can specify a series of keys and events
```sh
fzf --bind 'a:up,b:trigger(a,a,a)'
```
- Added support for `{*n}` and `{*nf}` placeholder.
- `{*n}` evaluates to the zero-based ordinal index of all matched items.
- `{*nf}` evaluates to the temporary file containing that.
- Bug fixes and improvements
- [neovim] Fixed margin background color when `&winborder` is used (#4453)
- Fixed rendering error when hiding a preview window without border (#4465)
- fix(shell): check for mawk existence before version check (#4468)
- Thanks to @LangLangBart and @akinomyoga
- Fixed `--no-header-lines-border` behavior (08027e7a)
0.64.0
------
- Added `multi` event that is triggered when the multi-selection has changed.
```sh
fzf --multi \
--bind 'ctrl-a:select-all,ctrl-d:deselect-all' \
--bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'
```
- [Halfwidth and fullwidth alphanumeric and punctuation characters](https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)) are now internally normalized to their ASCII equivalents to allow matching with ASCII queries.
```sh
echo | fzf -q abc
```
- Renamed `clear-selection` action to `clear-multi` for consistency.
- `clear-selection` remains supported as an alias for backward compatibility.
- Bug fixes
- Fixed a bug that could cause fzf to abort due to incorrect update ordering.
- Fixed a bug where some multi-selections were lost when using `exclude` or `change-nth`.
0.63.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_
- Added footer. The default border style for footer is `line`, which draws a single separator line.
```sh
fzf --reverse --footer "fzf: friend zone forever"
```
- Options
- `--footer[=STRING]`
- `--footer-border[=STYLE]`
- `--footer-label=LABEL`
- `--footer-label-pos=COL[:bottom]`
- Colors
- `footer`
- `footer-bg`
- `footer-border`
- `footer-label`
- Actions
- `change-footer`
- `transform-footer`
- `bg-transform-footer`
- `change-footer-label`
- `transform-footer-label`
- `bg-transform-footer-label`
- `line` border style is now allowed for all types of border except for `--list-border`.
```sh
fzf --height 50% --style full:line --preview 'cat {}' \
--bind 'focus:bg-transform-header(file {})+bg-transform-footer(wc {})'
```
- Added `{*}` placeholder flag that evaluates to all matched items.
```bash
seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}"
```
- Use this with caution, as it can make fzf sluggish for large lists.
- Added asynchronous transform actions with `bg-` prefix that run asynchronously in the background, along with `bg-cancel` action to cancel currently running `bg-transform` actions.
```sh
# Implement popup that disappears after 1 second
# * Use footer as the popup
# * Use `bell` to ring the terminal bell
# * Use `bg-transform-footer` to clear the footer after 1 second
# * Use `bg-cancel` to cancel currently running background transform actions
fzf --multi --list-border \
--bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
--bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
--bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
# It's okay for the commands to take a little while because they run in the background
GETTER='curl -s http://metaphorpsum.com/sentences/1'
fzf --style full --border --preview : \
--bind "focus:bg-transform-header:$GETTER" \
--bind "focus:+bg-transform-footer:$GETTER" \
--bind "focus:+bg-transform-border-label:$GETTER" \
--bind "focus:+bg-transform-preview-label:$GETTER" \
--bind "focus:+bg-transform-input-label:$GETTER" \
--bind "focus:+bg-transform-list-label:$GETTER" \
--bind "focus:+bg-transform-header-label:$GETTER" \
--bind "focus:+bg-transform-footer-label:$GETTER" \
--bind "focus:+bg-transform-ghost:$GETTER" \
--bind "focus:+bg-transform-prompt:$GETTER"
```
- Added support for full-line background color in the list section
```sh
for i in $(seq 16 255); do
echo -e "\x1b[48;5;${i}m\x1b[0Khello"
done | fzf --ansi
```
- SSH completion enhancements by @akinomyoga
- Bug fixes and improvements
0.62.0
------
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
```sh
# seoul256-light
fzf --style full --color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'
```
- Added `alt-bg` color to create striped lines to visually separate rows
```sh
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
bat --plain --language bash --color always |
fzf --read0 --ansi --reverse --multi \
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
```
- [fish] Improvements in CTRL-R binding (@bitraid)
- You can trigger CTRL-R in the middle of a command to insert the selected item
- You can delete history items with SHIFT-DEL
- Bug fixes and improvements
- Fixed unnecessary 100ms delay after `reload` (#4364)
- Fixed `selected-bg` not applied to colored items (#4372)
0.61.3
------
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
- More environment variables for child processes (#4356)
0.61.2 0.61.2
------ ------
- Fixed panic when using header border without pointer/marker (@phanen) - Fixed panic when using header border without pointer/marker (@phanen)

View File

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

View File

@@ -5,6 +5,15 @@ MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE)) ROOT_DIR := $(shell dirname $(MAKEFILE))
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE) SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
$(ROOT_DIR)/bin/fzf-tmux \
$(ROOT_DIR)/install \
$(ROOT_DIR)/uninstall \
$(ROOT_DIR)/shell/common.sh \
$(ROOT_DIR)/shell/update.sh \
$(ROOT_DIR)/shell/completion.bash \
$(ROOT_DIR)/shell/key-bindings.bash
ifdef FZF_VERSION ifdef FZF_VERSION
VERSION := $(FZF_VERSION) VERSION := $(FZF_VERSION)
else else
@@ -88,9 +97,14 @@ itest:
bench: bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
lint: $(SOURCES) test/*.rb test/lib/*.rb lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
shell/update.sh --check ${BASH_SCRIPTS}
fmt: $(SOURCES) $(BASH_SCRIPTS)
gofmt -s -w src
shell/update.sh ${BASH_SCRIPTS}
install: bin/fzf install: bin/fzf
@@ -189,4 +203,4 @@ update:
$(GO) get -u $(GO) get -u
$(GO) mod tidy $(GO) mod tidy
.PHONY: all generate build release test itest bench lint install clean docker docker-test update .PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt

View File

@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
let g:fzf_colors = let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'], \ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'], \ 'bg': ['bg', 'Normal'],
\ 'query': ['fg', 'Normal'],
\ 'hl': ['fg', 'Comment'], \ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
@@ -492,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi

File diff suppressed because one or more lines are too long

33
SECURITY.md Normal file
View File

@@ -0,0 +1,33 @@
# Security Reporting
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
## Reporting
To report a security vulnerability, please provide the following information:
1. **PROJECT**
- https://github.com/junegunn/fzf
2. **PUBLIC**
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
- If so, provide relevant links.
3. **DESCRIPTION**
- Provide a detailed description of the security vulnerability.
- Include as much information as possible to help us understand and address the issue.
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
## Confidentiality
We kindly ask you to keep the report confidential until a public announcement is made.
## Notes
- Vulnerabilities will be handled on a best-effort basis.
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
- You will be notified via email simultaneously with the public announcement.
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
Thank you for helping to improve the security of our project!

View File

@@ -49,9 +49,9 @@ if [[ ! $type =~ image/ ]]; then
fi fi
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES} dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
if [[ $dim = x ]]; then if [[ $dim == x ]]; then
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}') dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}'))); then
# Avoid scrolling issue when the Sixel image touches the bottom of the screen # Avoid scrolling issue when the Sixel image touches the bottom of the screen
# * https://github.com/junegunn/fzf/issues/2544 # * https://github.com/junegunn/fzf/issues/2544
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1)) dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))

View File

@@ -8,7 +8,7 @@ fail() {
} }
fzf="$(command which fzf)" || 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=()
opt="" opt=""
@@ -16,8 +16,8 @@ skip=""
swap="" swap=""
close="" close=""
term="" term=""
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}") [[ -n $LINES ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}") [[ -n $COLUMNS ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
tmux_version=$(tmux -V | sed 's/[^0-9.]//g') tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2") tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
@@ -47,7 +47,7 @@ help() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
arg="$1" arg="$1"
shift shift
[[ -z "$skip" ]] && case "$arg" in [[ -z $skip ]] && case "$arg" in
-) -)
term=1 term=1
;; ;;
@@ -58,19 +58,19 @@ while [[ $# -gt 0 ]]; do
echo "fzf-tmux (with fzf $("$fzf" --version))" echo "fzf-tmux (with fzf $("$fzf" --version))"
exit exit
;; ;;
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*) -p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
if [[ "$arg" =~ ^-[pwhxy] ]]; then if [[ $arg =~ ^-[pwhxy] ]]; then
[[ "$opt" =~ "-E" ]] || opt="-E" [[ $opt =~ "-E" ]] || opt="-E"
elif [[ "$arg" =~ ^.[lr] ]]; then elif [[ $arg =~ ^.[lr] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ $arg =~ ^.l ]]; then
opt="$opt -d" opt="$opt -d"
swap="; swap-pane -D ; select-pane -L" swap="; swap-pane -D ; select-pane -L"
close="; tmux swap-pane -D" close="; tmux swap-pane -D"
fi fi
else else
opt="" opt=""
if [[ "$arg" =~ ^.u ]]; then if [[ $arg =~ ^.u ]]; then
opt="$opt -d" opt="$opt -d"
swap="; swap-pane -D ; select-pane -U" swap="; swap-pane -D ; select-pane -U"
close="; tmux swap-pane -D" close="; tmux swap-pane -D"
@@ -79,7 +79,7 @@ while [[ $# -gt 0 ]]; do
if [[ ${#arg} -gt 2 ]]; then if [[ ${#arg} -gt 2 ]]; then
size="${arg:2}" size="${arg:2}"
else else
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
size="$1" size="$1"
shift shift
else else
@@ -87,37 +87,37 @@ while [[ $# -gt 0 ]]; do
fi fi
fi fi
if [[ "$arg" =~ ^-p ]]; then if [[ $arg =~ ^-p ]]; then
if [[ -n "$size" ]]; then if [[ -n $size ]]; then
w=${size%%,*} w=${size%%,*}
h=${size##*,} h=${size##*,}
opt="$opt -w$w -h$h" opt="$opt -w$w -h$h"
fi fi
elif [[ "$arg" =~ ^-[whxy] ]]; then elif [[ $arg =~ ^-[whxy] ]]; then
opt="$opt ${arg:0:2}$size" opt="$opt ${arg:0:2}$size"
elif [[ "$size" =~ %$ ]]; then elif [[ $size =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:${#size}-1}
if [[ $tmux_32 = 1 ]]; then if [[ $tmux_32 == 1 ]]; then
if [[ -n "$swap" ]]; then if [[ -n $swap ]]; then
opt="$opt -l $(( 100 - size ))%" opt="$opt -l $((100 - size))%"
else else
opt="$opt -l $size%" opt="$opt -l $size%"
fi fi
else else
if [[ -n "$swap" ]]; then if [[ -n $swap ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -p $((100 - size))"
else else
opt="$opt -p $size" opt="$opt -p $size"
fi fi
fi fi
else else
if [[ -n "$swap" ]]; then if [[ -n $swap ]]; then
if [[ "$arg" =~ ^.l ]]; then if [[ $arg =~ ^.l ]]; then
max=$columns max=$columns
else else
max=$lines max=$lines
fi fi
size=$(( max - size )) size=$((max - size))
[[ $size -lt 0 ]] && size=0 [[ $size -lt 0 ]] && size=0
opt="$opt -l $size" opt="$opt -l $size"
else else
@@ -135,10 +135,10 @@ while [[ $# -gt 0 ]]; do
args+=("$arg") args+=("$arg")
;; ;;
esac esac
[[ -n "$skip" ]] && args+=("$arg") [[ -n $skip ]] && args+=("$arg")
done done
if [[ -z "$TMUX" ]]; then if [[ -z $TMUX ]]; then
"$fzf" "${args[@]}" "$fzf" "${args[@]}"
exit $? exit $?
fi fi
@@ -149,7 +149,7 @@ fi
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux") 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
zoomed_without_popup=1 zoomed_without_popup=1
original_window=$(tmux display-message -p "#{window_id}") original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
@@ -165,22 +165,22 @@ fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") ) tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off' tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
else else
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') ) tmux_win_opts=($(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/'))
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off' tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
fi fi
cleanup() { cleanup() {
\rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Restore tmux window options # Restore tmux window options
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
eval "tmux ${tmux_win_opts[*]}" eval "tmux ${tmux_win_opts[*]}"
fi fi
# Remove temp window if we were zoomed without popup options # Remove temp window if we were zoomed without popup options
if [[ -n "$zoomed_without_popup" ]]; then if [[ -n $zoomed_without_popup ]]; then
tmux display-message -p "#{window_id}" > /dev/null tmux display-message -p "#{window_id}" > /dev/null
tmux swap-pane -t $original_window \; \ tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \ select-window -t $original_window \; \
@@ -197,10 +197,10 @@ trap 'cleanup 1' SIGUSR1
trap 'cleanup' EXIT trap 'cleanup' EXIT
envs="export TERM=$TERM " envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then if [[ $opt =~ "-E" ]]; then
if [[ $tmux_version = 3.2 ]]; then if [[ $tmux_version == 3.2 ]]; then
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS" FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
elif [[ $tmux_32 = 1 ]]; then elif [[ $tmux_32 == 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS" FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt" opt="-B $opt"
else else
@@ -211,8 +211,8 @@ fi
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")" envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")" [[ -n $RUNEWIDTH_EASTASIAN ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")" [[ -n $BAT_THEME ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
echo "$envs;" > "$argsf" echo "$envs;" > "$argsf"
# Build arguments to fzf # Build arguments to fzf
@@ -224,9 +224,9 @@ close="; trap - EXIT SIGINT SIGTERM $close"
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX") export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo2
if [[ "$opt" =~ "-E" ]]; then if [[ $opt =~ "-E" ]]; then
cat $fifo2 & cat $fifo2 &
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n $term ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
else else
mkfifo $fifo1 mkfifo $fifo1
@@ -239,7 +239,7 @@ if [[ "$opt" =~ "-E" ]]; then
fi fi
mkfifo -m o+w $fifo3 mkfifo -m o+w $fifo3
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n $term ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
else else
mkfifo $fifo1 mkfifo $fifo1
@@ -249,6 +249,9 @@ fi
tmux \ tmux \
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \ split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
$tmux_off_opts \ $tmux_off_opts \
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; } > /dev/null 2>&1 || {
"$fzf" "${args[@]}"
exit $?
}
cat $fifo2 cat $fifo2
exit "$(cat $fifo3)" exit "$(cat $fifo3)"

View File

@@ -503,7 +503,7 @@ LICENSE *fzf-license*
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
============================================================================== ==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

12
go.mod
View File

@@ -1,20 +1,20 @@
module github.com/junegunn/fzf module github.com/junegunn/fzf
require ( require (
github.com/charlievieth/fastwalk v1.0.10 github.com/charlievieth/fastwalk v1.0.14
github.com/gdamore/tcell/v2 v2.8.1 github.com/gdamore/tcell/v2 v2.9.0
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/rivo/uniseg v0.4.7 github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.30.0 golang.org/x/sys v0.35.0
golang.org/x/term v0.29.0 golang.org/x/term v0.34.0
) )
require ( require (
github.com/gdamore/encoding v1.0.1 // indirect github.com/gdamore/encoding v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.28.0 // indirect
) )
go 1.20 go 1.23.0

52
go.sum
View File

@@ -1,10 +1,9 @@
github.com/charlievieth/fastwalk v1.0.10 h1:0qUbvA2O+K+X+IrTfZTC0UH2DK5MOA+KjVfStAHUnGg= github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
github.com/charlievieth/fastwalk v1.0.10/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -14,35 +13,20 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -50,38 +34,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
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=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

53
install
View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.61.2 version=0.66.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -104,7 +104,7 @@ check_binary() {
link_fzf_in_path() { link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH" echo ' - Found in $PATH'
echo " - Creating symlink: bin/fzf -> $which_fzf" echo " - Creating symlink: bin/fzf -> $which_fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return check_binary && return
@@ -164,27 +164,28 @@ download() {
} }
# Try to download binary executable # Try to download binary executable
archi=$(uname -sm) archi=$(uname -smo 2> /dev/null || uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;; Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;; Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;; Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;; Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;; Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;; Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;; Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;; Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;; Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;; Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;; FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;; OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;; CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;; MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;; MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
Windows*\ *64) download fzf-$version-windows_amd64.zip ;; Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
*) binary_available=0 binary_error=1 ;; *) binary_available=0 binary_error=1 ;;
esac esac
@@ -214,7 +215,7 @@ if [ -n "$binary_error" ]; then
fi fi
fi fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ $* =~ "--bin" ]] && exit 0
for s in $shells; do for s in $shells; do
if ! command -v "$s" > /dev/null; then if ! command -v "$s" > /dev/null; then
@@ -241,7 +242,7 @@ fi
echo echo
for shell in $shells; do for shell in $shells; do
[[ "$shell" = fish ]] && continue [[ $shell == fish ]] && continue
src=${prefix_expand}.${shell} src=${prefix_expand}.${shell}
echo -n "Generate $src ... " echo -n "Generate $src ... "
@@ -265,7 +266,7 @@ fi
EOF EOF
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
if [[ "$shell" = zsh ]]; then if [[ $shell == zsh ]]; then
echo "source <(fzf --$shell)" >> "$src" echo "source <(fzf --$shell)" >> "$src"
else else
echo "eval \"\$(fzf --$shell)\"" >> "$src" echo "eval \"\$(fzf --$shell)\"" >> "$src"
@@ -285,7 +286,7 @@ EOF
done done
# fish # fish
if [[ "$shells" =~ fish ]]; then if [[ $shells =~ fish ]]; then
echo -n "Update fish_user_paths ... " echo -n "Update fish_user_paths ... "
fish << EOF fish << EOF
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
@@ -317,7 +318,7 @@ append_line() {
sed 's/^/ Line /' <<< "$lines" sed 's/^/ Line /' <<< "$lines"
update=0 update=0
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
echo " - But they all seem to be commented" echo " - But they all seem to be commented"
ask " - Continue modifying $file?" ask " - Continue modifying $file?"
update=$? update=$?
@@ -355,12 +356,12 @@ if [ $update_config -eq 2 ]; then
fi fi
echo echo
for shell in $shells; do for shell in $shells; do
[[ "$shell" = fish ]] && continue [[ $shell == fish ]] && continue
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}" append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
done done
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then if [ $key_bindings -eq 1 ] && [[ $shells =~ fish ]]; then
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish" bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
if [ ! -e "$bind_file" ]; then if [ ! -e "$bind_file" ]; then
mkdir -p "${fish_dir}/functions" mkdir -p "${fish_dir}/functions"
@@ -385,13 +386,13 @@ fi
if [ $update_config -eq 1 ]; then if [ $update_config -eq 1 ]; then
echo 'Finished. Restart your shell or reload config file.' echo 'Finished. Restart your shell or reload config file.'
if [[ "$shells" =~ bash ]]; then if [[ $shells =~ bash ]]; then
echo -n ' source ~/.bashrc # bash' echo -n ' source ~/.bashrc # bash'
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)' [[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
echo echo
fi fi
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' [[ $shells =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
echo echo
echo 'Use uninstall script to remove fzf.' echo 'Use uninstall script to remove fzf.'
echo echo

View File

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

View File

@@ -11,7 +11,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version = "0.61" var version = "0.66"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Apr 2025" "fzf 0.61.2" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Oct 2025" "fzf 0.66.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013-2024 Junegunn Choi Copyright (c) 2013-2025 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2025" "fzf 0.61.2" "fzf - a command-line fuzzy finder" .TH fzf 1 "Oct 2025" "fzf 0.66.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -242,15 +242,15 @@ Apply a style preset [default|minimal|full[:BORDER_STYLE]]
.TP .TP
.BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..." .BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. color mappings. Each entry is separated by a comma and/or whitespaces.
.RS .RS
.B BASE SCHEME: .B BASE SCHEME:
(default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR) (default: \fBdark\fR on 256-color terminal, otherwise \fBbase16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
\fBdark \fRColor scheme for dark 256-color terminal \fBdark \fRColor scheme for dark terminal
\fBlight \fRColor scheme for light 256-color terminal \fBlight \fRColor scheme for light terminal
\fB16 \fRColor scheme for 16-color terminal \fBbase16 \fRColor scheme using base 16 colors (alias: \fB16\fR)
\fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR) \fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR)
.B COLOR NAMES: .B COLOR NAMES:
@@ -262,15 +262,18 @@ color mappings.
\fBlist\-bg \fRList section background \fBlist\-bg \fRList section background
\fBselected\-bg \fRSelected line background \fBselected\-bg \fRSelected line background
\fBpreview\-bg \fRPreview window background \fBpreview\-bg \fRPreview window background
\fBinput\-bg \fRInput window background (\fB\-\-input\-border\fR) \fBinput\-bg \fRInput window background
\fBheader\-bg \fRHeader window background (\fB\-\-header\-border\fR) \fBheader\-bg \fRHeader window background
\fBfooter\-bg \fRFooter window background
\fBhl \fRHighlighted substrings \fBhl \fRHighlighted substrings
\fBselected\-hl \fRHighlighted substrings in the selected line \fBselected\-hl \fRHighlighted substrings in the selected line
\fBcurrent\-fg (fg+) \fRText (current line) \fBcurrent\-fg (fg+) \fRText (current line)
\fBcurrent\-bg (bg+) \fRBackground (current line) \fBcurrent\-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left \fBgutter \fRGutter on the left
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line) \fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
\fBalt\-bg \fRAlternate background color to create striped lines
\fBquery (input\-fg) \fRQuery string \fBquery (input\-fg) \fRQuery string
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
\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)
\fBborder \fRBorder around the window (\fB\-\-border\fR and \fB\-\-preview\fR) \fBborder \fRBorder around the window (\fB\-\-border\fR and \fB\-\-preview\fR)
@@ -282,17 +285,21 @@ color mappings.
\fBpreview\-scrollbar \fRScrollbar \fBpreview\-scrollbar \fRScrollbar
\fBinput\-border \fRBorder around the input window (\fB\-\-input\-border\fR) \fBinput\-border \fRBorder around the input window (\fB\-\-input\-border\fR)
\fBheader\-border \fRBorder around the header window (\fB\-\-header\-border\fR) \fBheader\-border \fRBorder around the header window (\fB\-\-header\-border\fR)
\fBfooter\-border \fRBorder around the footer window (\fB\-\-footer\-border\fR)
\fBlabel \fRBorder label (\fB\-\-border\-label\fR, \fB\-\-list\-label\fR, \fB\-\-input\-label\fR, and \fB\-\-preview\-label\fR) \fBlabel \fRBorder label (\fB\-\-border\-label\fR, \fB\-\-list\-label\fR, \fB\-\-input\-label\fR, and \fB\-\-preview\-label\fR)
\fBlist\-label \fRBorder label of the list section (\fB\-\-list\-label\fR) \fBlist\-label \fRBorder label of the list section (\fB\-\-list\-label\fR)
\fBpreview\-label \fRBorder label of the preview window (\fB\-\-preview\-label\fR) \fBpreview\-label \fRBorder label of the preview window (\fB\-\-preview\-label\fR)
\fBinput\-label \fRBorder label of the input window (\fB\-\-input\-label\fR) \fBinput\-label \fRBorder label of the input window (\fB\-\-input\-label\fR)
\fBheader\-label \fRBorder label of the header window (\fB\-\-header\-label\fR) \fBheader\-label \fRBorder label of the header window (\fB\-\-header\-label\fR)
\fBfooter\-label \fRBorder label of the footer window (\fB\-\-footer\-label\fR)
\fBprompt \fRPrompt \fBprompt \fRPrompt
\fBpointer \fRPointer to the current line \fBpointer \fRPointer to the current line
\fBmarker \fRMulti\-select marker \fBmarker \fRMulti\-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader (header\-fg) \fRHeader \fBheader (header\-fg) \fRHeader
\fBfooter (footer\-fg) \fRFooter
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes) \fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
\fBnomatch \fRNon-matching items in raw mode (default: \fBdim\fR)
.B ANSI COLORS: .B ANSI COLORS:
\fB\-1 \fRDefault terminal foreground/background color \fB\-1 \fRDefault terminal foreground/background color
@@ -318,7 +325,8 @@ color mappings.
\fB#rrggbb \fR24-bit colors \fB#rrggbb \fR24-bit colors
.B ANSI ATTRIBUTES: (Only applies to foreground colors) .B ANSI ATTRIBUTES: (Only applies to foreground colors)
\fBregular \fRClears previously set attributes; should precede the other ones \fBregular \fRClear previously set attributes; should precede the other ones
\fBstrip \fRRemove colors
\fBbold\fR \fBbold\fR
\fBunderline\fR \fBunderline\fR
\fBreverse\fR \fBreverse\fR
@@ -337,7 +345,19 @@ color mappings.
# Seoul256 theme with 24-bit colors # Seoul256 theme with 24-bit colors
fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\ fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
\-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\ \-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR \-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'
# Seoul256 light theme with 24-bit colors, each entry separated by whitespaces
fzf \-\-style full \-\-color='
fg:#616161 fg+:#616161
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
hl:#719872 hl+:#719899
pointer:#e12672 marker:#e17899
header:#719872
spinner:#719899 info:#727100
prompt:#0099bd query:#616161
border:#e1e1e1
'\fR
.RE .RE
.TP .TP
.B "\-\-no\-color" .B "\-\-no\-color"
@@ -489,6 +509,8 @@ Draw border around the finder
.br .br
.BR vertical " Vertical lines on each side of the finder" .BR vertical " Vertical lines on each side of the finder"
.br .br
.BR line " Single line border (position automatically determined)"
.br
.BR top " (up)" .BR top " (up)"
.br .br
.BR bottom " (down)" .BR bottom " (down)"
@@ -504,6 +526,9 @@ If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly 2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly
rendered, set \fB\-\-no\-unicode\fR. rendered, set \fB\-\-no\-unicode\fR.
\fBline\fR style draws a single separator line at the top when \fB\-\-height\fR
is used.
.TP .TP
.BI "\-\-border\-label" [=LABEL] .BI "\-\-border\-label" [=LABEL]
Label to print on the horizontal border line. Should be used with one of the Label to print on the horizontal border line. Should be used with one of the
@@ -573,6 +598,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
.B "\-\-no\-multi\-line" .B "\-\-no\-multi\-line"
Disable multi-line display of items when using \fB\-\-read0\fR Disable multi-line display of items when using \fB\-\-read0\fR
.TP .TP
.B "\-\-raw"
Enable raw mode where non-matching items are also displayed in a dimmed color.
.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
@@ -620,6 +648,9 @@ on the center of the screen.
.BI "\-\-jump\-labels=" "CHARS" .BI "\-\-jump\-labels=" "CHARS"
Label characters for \fBjump\fR mode. Label characters for \fBjump\fR mode.
.TP .TP
.BI "\-\-gutter=" "CHAR"
Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given)
.TP
.BI "\-\-pointer=" "STR" .BI "\-\-pointer=" "STR"
Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR) Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR)
.TP .TP
@@ -647,7 +678,8 @@ Do not display scrollbar. A synonym for \fB\-\-scrollbar=''\fB
.TP .TP
.BI "\-\-list\-border" [=STYLE] .BI "\-\-list\-border" [=STYLE]
Draw border around the list section Draw border around the list section. \fBline\fR style is not supported for
this border.
.TP .TP
.BI "\-\-list\-label" [=LABEL] .BI "\-\-list\-label" [=LABEL]
@@ -734,7 +766,8 @@ actions are affected:
\fBkill\-word\fR \fBkill\-word\fR
.TP .TP
.BI "\-\-input\-border" [=STYLE] .BI "\-\-input\-border" [=STYLE]
Draw border around the input section Draw border around the input section. \fBline\fR style draws a single separator
line between the input section and the list section.
.TP .TP
.BI "\-\-input\-label" [=LABEL] .BI "\-\-input\-label" [=LABEL]
@@ -768,13 +801,16 @@ fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
the preview command can determine the position of the preview window. the preview command can determine the position of the preview window.
A placeholder expression starting with \fB+\fR flag will be replaced to the A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection space-separated list of the selected items (or the current item if no selection
was made) individually quoted. was made) individually quoted.
e.g. e.g.
\fBfzf \-\-multi \-\-preview='head \-10 {+}' \fBfzf \-\-multi \-\-preview='head \-10 {+}'
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
Similarly, a placeholder expression starting with \fB*\fR flag will be replaced
to the space-separated list of all matched items individually quoted.
Each expression expands to a quoted string, so that it's safe to pass it as an Each expression expands to a quoted string, so that it's safe to pass it as an
argument to an external command. So you should not manually add quotes around argument to an external command. So you should not manually add quotes around
the curly braces. But if you don't want this behavior, you can put the curly braces. But if you don't want this behavior, you can put
@@ -786,14 +822,13 @@ from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
A placeholder expression with \fBf\fR flag is replaced to the path of A placeholder expression with \fBf\fR flag is replaced to the path of
a temporary file that holds the evaluated list. This is useful when you a temporary file that holds the evaluated list. This is useful when you
multi-select a large number of items and the length of the evaluated string may pass a large number of items and the length of the evaluated string may
exceed \fBARG_MAX\fR. exceed \fBARG_MAX\fR.
e.g. e.g.
\fB# Press CTRL\-A to select 100K items and see the sum of all the numbers. \fB# See the sum of all the matched numbers
# This won't work properly without 'f' flag due to ARG_MAX limit. # This won't work properly without 'f' flag due to ARG_MAX limit.
seq 100000 | fzf \-\-multi \-\-bind ctrl\-a:select\-all \\ seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
\-\-preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
Also, Also,
@@ -834,8 +869,7 @@ e.g.
.TP .TP
.BI "\-\-preview\-border" [=STYLE] .BI "\-\-preview\-border" [=STYLE]
Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other Short for \fB\-\-preview\-window=border\-STYLE\fR. \fBline\fR style draws
styles, \fBline\fR style is also supported for preview border, which draws
a single separator line between the preview window and the rest of the a single separator line between the preview window and the rest of the
interface. interface.
@@ -987,10 +1021,12 @@ The first N lines of the input are treated as the sticky header. When
lines that follow. lines that follow.
.TP .TP
.B "\-\-header\-first" .B "\-\-header\-first"
Print header before the prompt line Print header before the prompt line. When both normal header and header lines
(\fB\-\-header\-lines\fR) are present, this applies only to the normal header.
.TP .TP
.BI "\-\-header\-border" [=STYLE] .BI "\-\-header\-border" [=STYLE]
Draw border around the header section Draw border around the header section. \fBline\fR style draws a single
separator line between the header window and the list section.
.TP .TP
.BI "\-\-header\-label" [=LABEL] .BI "\-\-header\-label" [=LABEL]
@@ -1004,7 +1040,30 @@ Position of the header label
.BI "\-\-header\-lines\-border" [=STYLE] .BI "\-\-header\-lines\-border" [=STYLE]
Display header from \fB--header\-lines\fR with a separate border. Pass Display header from \fB--header\-lines\fR with a separate border. Pass
\fBnone\fR to still separate the header lines but without a border. To combine \fBnone\fR to still separate the header lines but without a border. To combine
two headers, use \fB\-\-no\-header\-lines\-border\fR. two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
a single separator line between the header lines and the list section.
.SS FOOTER
.TP
.BI "\-\-footer=" "STR"
The given string will be printed as the sticky footer. The lines are displayed
in the given order from top to bottom regardless of \fB\-\-layout\fR option, and
are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even when
\fB\-\-ansi\fR is not set.
.TP
.BI "\-\-footer\-border" [=STYLE]
Draw border around the footer section. \fBline\fR style draws a single
separator line between the footer and the list section.
.TP
.BI "\-\-footer\-label" [=LABEL]
Label to print on the footer border
.TP
.BI "\-\-footer\-label\-pos" [=N[:top|bottom]]
Position of the footer label
.SS SCRIPTING .SS SCRIPTING
.TP .TP
@@ -1074,19 +1133,25 @@ On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
set. set.
.TP .TP
.B "\-\-listen[=[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]" .B "\-\-listen[=SOCKET_PATH|[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 or Unix socket. It allows
to send actions to perform via POST method. external processes to send actions to perform via POST method and query the
program state via GET method. For the argument to be recognized as a socket
path, it must have \fB.sock\fR extension.
- If the port number is omitted or given as 0, fzf will automatically choose - If the port number is omitted or given as 0, fzf will automatically choose
a port and export it as \fBFZF_PORT\fR environment variable to the child processes a port and export it as \fBFZF_PORT\fR environment variable to the child processes.
- If a Unix socket path is given, fzf will create a Unix domain socket at the
given path. The existing file will be removed. The path to the socket file
is exported as \fBFZF_SOCK\fR environment variable.
- If \fBFZF_API_KEY\fR environment variable is set, the server would require - If \fBFZF_API_KEY\fR environment variable is set, the server would require
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header sending an API key with the same value in the \fBx\-api\-key\fR HTTP header.
- \fBFZF_API_KEY\fR is required for a non-localhost listen address - \fBFZF_API_KEY\fR is required for a non-localhost listen address.
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR - To allow remote process execution, use \fB\-\-listen\-unsafe\fR.
e.g. e.g.
\fB# Start HTTP server on port 6266 \fB# Start HTTP server on port 6266
@@ -1095,11 +1160,6 @@ e.g.
# Send action to the server # Send action to the server
curl \-XPOST localhost:6266 \-d 'reload(seq 100)+change\-prompt(hundred> )' curl \-XPOST localhost:6266 \-d 'reload(seq 100)+change\-prompt(hundred> )'
# Get program state in JSON format (experimental)
# * Make sure NOT to access this endpoint from execute/transform actions
# as it will result in a timeout
curl localhost:6266
# Start HTTP server on port 6266 with remote connections allowed # Start HTTP server on port 6266 with remote connections allowed
# * Listening on non-localhost address requires using an API key # * Listening on non-localhost address requires using an API key
export FZF_API_KEY="$(head \-c 32 /dev/urandom | base64)" export FZF_API_KEY="$(head \-c 32 /dev/urandom | base64)"
@@ -1110,6 +1170,36 @@ 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'
# Get program state in JSON format (experimental)
# - GET Parameters:
# - limit: number of items to return (default: 100)
# - offset: number of items to skip (default: 0)
curl localhost:6266
# Automatically select items with .txt extension
fzf \-\-multi \-\-sync \-\-listen \-\-bind 'load:transform:
pos=1
curl \-s localhost:$FZF_PORT?limit=1000 | jq \-r .matches[].text | while read \-r text; do
if [[ $text =~ \\.txt$ ]]; then
echo \-n "+pos($pos)+select"
fi
pos=$((pos + 1))
done
echo +first
'
\fR
Here is an example script that uses a Unix socket instead of a TCP port.
\fB
fzf --listen=/tmp/fzf.sock
# GET
curl --unix-socket /tmp/fzf.sock http
# POST
curl --unix-socket /tmp/fzf.sock http -d up
\fR \fR
.SS DIRECTORY TRAVERSAL .SS DIRECTORY TRAVERSAL
@@ -1263,6 +1353,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin" .BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
.br .br
.BR FZF_DIRECTION " Direction of the list (\fBup\fR or \fBdown\fR)"
.br
.BR FZF_TOTAL_COUNT " Total number of items" .BR FZF_TOTAL_COUNT " Total number of items"
.br .br
.BR FZF_MATCH_COUNT " Number of matched items" .BR FZF_MATCH_COUNT " Number of matched items"
@@ -1279,16 +1371,28 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
.br .br
.BR FZF_GHOST " Ghost string"
.br
.BR FZF_POINTER " Pointer string"
.br
.BR FZF_PREVIEW_LABEL " Preview label string" .BR FZF_PREVIEW_LABEL " Preview label string"
.br .br
.BR FZF_BORDER_LABEL " Border label string" .BR FZF_BORDER_LABEL " Border label string"
.br .br
.BR FZF_LIST_LABEL " List label string"
.br
.BR FZF_INPUT_LABEL " Input label string"
.br
.BR FZF_HEADER_LABEL " Header label string"
.br
.BR FZF_ACTION " The name of the last action performed" .BR FZF_ACTION " The name of the last action performed"
.br .br
.BR FZF_KEY " The name of the last key pressed" .BR FZF_KEY " The name of the last key pressed"
.br .br
.BR FZF_PORT " Port number when \-\-listen option is used" .BR FZF_PORT " Port number when \-\-listen option is used"
.br .br
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
.br
.BR FZF_PREVIEW_TOP " Top position of the preview window" .BR FZF_PREVIEW_TOP " Top position of the preview window"
.br .br
.BR FZF_PREVIEW_LEFT " Left position of the preview window" .BR FZF_PREVIEW_LEFT " Left position of the preview window"
@@ -1296,6 +1400,8 @@ fzf exports the following environment variables to its child processes.
.BR FZF_PREVIEW_LINES " Number of lines in the preview window" .BR FZF_PREVIEW_LINES " Number of lines in the preview window"
.br .br
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window" .BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
.br
.BR FZF_RAW " Only in raw mode. 1 if the current item matches, 0 otherwise"
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
@@ -1399,12 +1505,22 @@ e.g.
.br .br
\fIalt\-right\fR \fIalt\-right\fR
.br .br
\fIalt\-home\fR
.br
\fIalt\-end\fR
.br
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
.br
\fIalt\-delete\fR
.br
\fIalt\-page\-up\fR
.br
\fIalt\-page\-down\fR
.br
\fIalt\-enter\fR \fIalt\-enter\fR
.br .br
\fIalt\-space\fR \fIalt\-space\fR
.br .br
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
.br
\fItab\fR \fItab\fR
.br .br
\fIshift\-tab\fR (\fIbtab\fR) \fIshift\-tab\fR (\fIbtab\fR)
@@ -1431,6 +1547,26 @@ e.g.
.br .br
\fIpage\-down\fR (\fIpgdn\fR) \fIpage\-down\fR (\fIpgdn\fR)
.br .br
\fIctrl\-up\fR
.br
\fIctrl\-down\fR
.br
\fIctrl\-left\fR
.br
\fIctrl\-right\fR
.br
\fIctrl\-home\fR
.br
\fIctrl\-end\fR
.br
\fIctrl\-backspace\fR (\fIctrl\-bspace\fR \fIctrl\-bs\fR)
.br
\fIctrl\-delete\fR
.br
\fIctrl\-page\-up\fR
.br
\fIctrl\-page\-down\fR
.br
\fIshift\-up\fR \fIshift\-up\fR
.br .br
\fIshift\-down\fR \fIshift\-down\fR
@@ -1439,8 +1575,16 @@ e.g.
.br .br
\fIshift\-right\fR \fIshift\-right\fR
.br .br
\fIshift\-home\fR
.br
\fIshift\-end\fR
.br
\fIshift\-delete\fR \fIshift\-delete\fR
.br .br
\fIshift\-page\-up\fR
.br
\fIshift\-page\-down\fR
.br
\fIalt\-shift\-up\fR \fIalt\-shift\-up\fR
.br .br
\fIalt\-shift\-down\fR \fIalt\-shift\-down\fR
@@ -1449,6 +1593,72 @@ e.g.
.br .br
\fIalt\-shift\-right\fR \fIalt\-shift\-right\fR
.br .br
\fIalt\-shift\-home\fR
.br
\fIalt\-shift\-end\fR
.br
\fIalt\-shift\-delete\fR
.br
\fIalt\-shift\-page\-up\fR
.br
\fIalt\-shift\-page\-down\fR
.br
\fIctrl\-alt\-up\fR
.br
\fIctrl\-alt\-down\fR
.br
\fIctrl\-alt\-left\fR
.br
\fIctrl\-alt\-right\fR
.br
\fIctrl\-alt\-home\fR
.br
\fIctrl\-alt\-end\fR
.br
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR)
.br
\fIctrl\-alt\-delete\fR
.br
\fIctrl\-alt\-page\-up\fR
.br
\fIctrl\-alt\-page\-down\fR
.br
\fIctrl\-shift\-up\fR
.br
\fIctrl\-shift\-down\fR
.br
\fIctrl\-shift\-left\fR
.br
\fIctrl\-shift\-right\fR
.br
\fIctrl\-shift\-home\fR
.br
\fIctrl\-shift\-end\fR
.br
\fIctrl\-shift\-delete\fR
.br
\fIctrl\-shift\-page\-up\fR
.br
\fIctrl\-shift\-page\-down\fR
.br
\fIctrl\-alt\-shift\-up\fR
.br
\fIctrl\-alt\-shift\-down\fR
.br
\fIctrl\-alt\-shift\-left\fR
.br
\fIctrl\-alt\-shift\-right\fR
.br
\fIctrl\-alt\-shift\-home\fR
.br
\fIctrl\-alt\-shift\-end\fR
.br
\fIctrl\-alt\-shift\-delete\fR
.br
\fIctrl\-alt\-shift\-page\-up\fR
.br
\fIctrl\-alt\-shift\-page\-down\fR
.br
\fIleft\-click\fR \fIleft\-click\fR
.br .br
\fIright\-click\fR \fIright\-click\fR
@@ -1473,6 +1683,8 @@ e.g.
.br .br
or any single character or any single character
Note that some terminal emulators may not support \fIctrl-*\fR bindings.
.SS AVAILABLE EVENTS: .SS AVAILABLE EVENTS:
\fIstart\fR \fIstart\fR
.RS .RS
@@ -1535,6 +1747,10 @@ e.g.
# Beware not to introduce an infinite loop # Beware not to introduce an infinite loop
seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR
.RE .RE
\fImulti\fR
.RS
Triggered when the multi\-selection has changed.
.RE
\fIone\fR \fIone\fR
.RS .RS
@@ -1604,6 +1820,14 @@ e.g.
)'\fR )'\fR
.RE .RE
\fIclick\-footer\fR
.RS
Triggered when a mouse click occurs within the footer. Sets
\fBFZF_CLICK_FOOTER_LINE\fR and \fBFZF_CLICK_FOOTER_COLUMN\fR environment
variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR
if clicked on a word.
.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.
@@ -1613,13 +1837,17 @@ A key or an event can be bound to one or more of the following actions.
\fBaccept\-non\-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) \fBaccept\-non\-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBaccept\-or\-print\-query\fR (same as \fBaccept\fR except that it prints the query when there's no match) \fBaccept\-or\-print\-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
\fBbackward\-char\fR \fIctrl\-b left\fR \fBbackward\-char\fR \fIctrl\-b left\fR
\fBbackward\-delete\-char\fR \fIctrl\-h bspace\fR \fBbackward\-delete\-char\fR \fIctrl\-h ctrl\-bspace bspace\fR
\fBbackward\-delete\-char/eof\fR (same as \fBbackward\-delete\-char\fR except aborts fzf if query is empty) \fBbackward\-delete\-char/eof\fR (same as \fBbackward\-delete\-char\fR except aborts fzf if query is empty)
\fBbackward\-kill\-subword\fR
\fBbackward\-kill\-word\fR \fIalt\-bs\fR \fBbackward\-kill\-word\fR \fIalt\-bs\fR
\fBbackward\-subword\fR
\fBbackward\-word\fR \fIalt\-b shift\-left\fR \fBbackward\-word\fR \fIalt\-b shift\-left\fR
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details) \fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR \fBbeginning\-of\-line\fR \fIctrl\-a home\fR
\fBbell\fR (ring the terminal bell) \fBbell\fR (ring the terminal bell)
\fBbest\fR (move to the best match; same as \fBfirst\fR if raw mode is disabled)
\fBbg\-cancel\fR (cancel background transform processes)
\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\-ghost(...)\fR (change ghost text to the given string) \fBchange\-ghost(...)\fR (change ghost text to the given string)
@@ -1637,15 +1865,19 @@ A key or an event can be bound to one or more of the following actions.
\fBchange\-prompt(...)\fR (change prompt to the given string) \fBchange\-prompt(...)\fR (change prompt to the given string)
\fBchange\-query(...)\fR (change query string to the given string) \fBchange\-query(...)\fR (change query string to the given string)
\fBclear\-screen\fR \fIctrl\-l\fR \fBclear\-screen\fR \fIctrl\-l\fR
\fBclear\-selection\fR (clear multi\-selection) \fBclear\-multi\fR (clear multi\-selection)
\fBclose\fR (close preview window if open, abort fzf otherwise) \fBclose\fR (close preview window if open, abort fzf otherwise)
\fBclear\-query\fR (clear query string) \fBclear\-query\fR (clear query string)
\fBdelete\-char\fR \fIdel\fR \fBdelete\-char\fR \fIdel\fR
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty) \fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
\fBdeselect\fR \fBdeselect\fR
\fBdeselect\-all\fR (deselect all matches) \fBdeselect\-all\fR (deselect all matches; to also clear non-matching selections, use \fBclear\-multi\fR)
\fBdisable\-raw\fR (disable raw mode)
\fBdisable\-search\fR (disable search functionality) \fBdisable\-search\fR (disable search functionality)
\fBdown\fR \fIctrl\-j ctrl\-n down\fR \fBdown\fR \fIctrl\-j down\fR
\fBdown\-match\fR \fIctrl\-n\fR \fIalt\-down\fR (move to the match below the cursor)
\fBdown\-selected\fR (move to the selected item below the cursor)
\fBenable\-raw\fR (enable raw mode)
\fBenable\-search\fR (enable search functionality) \fBenable\-search\fR (enable search functionality)
\fBend\-of\-line\fR \fIctrl\-e end\fR \fBend\-of\-line\fR \fIctrl\-e end\fR
\fBexclude\fR (exclude the current item from the result) \fBexclude\fR (exclude the current item from the result)
@@ -1654,14 +1886,16 @@ A key or an event can be bound to one or more of the following actions.
\fBexecute\-silent(...)\fR (see below for the details) \fBexecute\-silent(...)\fR (see below for the details)
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR) \fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
\fBforward\-char\fR \fIctrl\-f right\fR \fBforward\-char\fR \fIctrl\-f right\fR
\fBforward\-subword\fR
\fBforward\-word\fR \fIalt\-f shift\-right\fR \fBforward\-word\fR \fIalt\-f shift\-right\fR
\fBignore\fR \fBignore\fR
\fBjump\fR (EasyMotion-like 2-keystroke movement) \fBjump\fR (EasyMotion-like 2-keystroke movement)
\fBkill\-line\fR \fBkill\-line\fR
\fBkill\-subword\fR
\fBkill\-word\fR \fIalt\-d\fR \fBkill\-word\fR \fIalt\-d\fR
\fBlast\fR (move to the last match; same as \fBpos(\-1)\fR) \fBlast\fR (move to the last match; same as \fBpos(\-1)\fR)
\fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR) \fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR)
\fBnext\-selected\fR (move to the next selected item) \fBnext\-selected\fR (synonym to \fBdown\-selected\fR)
\fBpage\-down\fR \fIpgdn\fR \fBpage\-down\fR \fIpgdn\fR
\fBpage\-up\fR \fIpgup\fR \fBpage\-up\fR \fIpgup\fR
\fBhalf\-page\-down\fR \fBhalf\-page\-down\fR
@@ -1674,7 +1908,7 @@ A key or an event can be bound to one or more of the following actions.
\fBoffset\-middle\fR (place the current item is in the middle of the screen) \fBoffset\-middle\fR (place the current item is in the middle of the screen)
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
\fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR) \fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR)
\fBprev\-selected\fR (move to the previous selected item) \fBprev\-selected\fR (synonym to \fBup\-selected\fR)
\fBpreview(...)\fR (see below for the details) \fBpreview(...)\fR (see below for the details)
\fBpreview\-down\fR \fIshift\-down\fR \fBpreview\-down\fR \fIshift\-down\fR
\fBpreview\-up\fR \fIshift\-up\fR \fBpreview\-up\fR \fIshift\-up\fR
@@ -1709,6 +1943,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle\-multi\-line\fR \fBtoggle\-multi\-line\fR
\fBtoggle\-preview\fR \fBtoggle\-preview\fR
\fBtoggle\-preview\-wrap\fR \fBtoggle\-preview\-wrap\fR
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
\fBtoggle\-search\fR (toggle search functionality) \fBtoggle\-search\fR (toggle search functionality)
\fBtoggle\-sort\fR \fBtoggle\-sort\fR
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR)) \fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
@@ -1730,13 +1965,19 @@ A key or an event can be bound to one or more of the following actions.
\fBtransform\-prompt(...)\fR (transform prompt string using an external command) \fBtransform\-prompt(...)\fR (transform prompt string using an external command)
\fBtransform\-query(...)\fR (transform query string using an external command) \fBtransform\-query(...)\fR (transform query string using an external command)
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command) \fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
\fBtrigger(...)\fR (trigger actions bound to a comma-separated list of keys and events)
\fBunbind(...)\fR (unbind bindings) \fBunbind(...)\fR (unbind bindings)
\fBunix\-line\-discard\fR \fIctrl\-u\fR \fBunix\-line\-discard\fR \fIctrl\-u\fR
\fBunix\-word\-rubout\fR \fIctrl\-w\fR \fBunix\-word\-rubout\fR \fIctrl\-w\fR
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled) \fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
\fBup\fR \fIctrl\-k ctrl\-p up\fR \fBup\fR \fIctrl\-k up\fR
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
\fBup\-selected\fR (move to the selected item above the cursor)
\fByank\fR \fIctrl\-y\fR \fByank\fR \fIctrl\-y\fR
Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR
variant that runs the command in the background.
.SS ACTION COMPOSITION .SS ACTION COMPOSITION
Multiple actions can be chained using \fB+\fR separator. Multiple actions can be chained using \fB+\fR separator.
@@ -1861,6 +2102,26 @@ e.g.
echo "change\-header:Invalid selection"' echo "change\-header:Invalid selection"'
\fR \fR
A common mistake when writing a \fBtransform\fR action is not escaping
placeholder expressions when passing them back to fzf. In the following
example, if you don't escape \fB{}\fR, fzf will immediately replace it with the
single-quoted string of the current item. This causes single quotes to appear
in the header and footer, and the script will break if any item contains
double-quote characters.
\fBfzf \-\-bind 'focus:transform:[[ $FZF_ACTION =~ up ]] &&
echo "change\-header()+transform\-footer:echo \\{}" ||
echo "change\-footer()+transform\-header:echo \\{}"'\fR
.SS TRANSFORM IN THE BACKGROUND
Transform actions are synchronous, meaning fzf becomes unresponsive while the
command runs. To avoid this, each \fBtransform*\fR action has a corresponding
\fBbg\-transform*\fR variant that runs in the background. Unless you need to
chain multiple transform actions where later ones depend on earlier results,
prefer using the \fBbg\fR variant. To cancel currently running background
transform processes, use \fBbg\-cancel\fR action.
.SS PREVIEW BINDING .SS PREVIEW BINDING
With \fBpreview(...)\fR action, you can specify multiple different preview With \fBpreview(...)\fR action, you can specify multiple different preview

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013-2024 Junegunn Choi " Copyright (c) 2013-2025 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -553,8 +553,15 @@ try
let height = s:calc_size(&lines, dict.down, dict) let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --no-tmux --height='.height let optstr .= ' --no-tmux --height='.height
endif endif
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
" Add 1-column horizontal margin
let optstr = join(['--margin 0,1', optstr])
else
" 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])
endif
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term if use_term
@@ -1020,8 +1027,23 @@ if has('nvim')
let buf = nvim_create_buf(v:false, v:true) let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts) let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let win = nvim_open_win(buf, v:true, opts) let win = nvim_open_win(buf, v:true, opts)
silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
call setwinvar(win, '&colorcolumn', '') call setwinvar(win, '&colorcolumn', '')
" Colors
try
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
let rules = get(g:, 'fzf_colors', {})
if has_key(rules, 'bg')
let color = call('s:get_color', rules.bg)
if len(color)
let ns = nvim_create_namespace('fzf_popup')
let hl = nvim_set_hl(ns, 'Normal',
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
call nvim_win_set_hl_ns(win, ns)
endif
endif
catch
endtry
return buf return buf
endfunction endfunction
else else

37
shell/common.sh Normal file
View File

@@ -0,0 +1,37 @@
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_exec_awk() {
# This function performs `exec awk "$@"` safely by working around awk
# compatibility issues.
#
# To reduce an extra fork, this function performs "exec" so is expected to be
# run as the last command in a subshell.
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
# with an ancient implementation of 1977 awk in the original UNIX. It
# lacks many features of POSIX awk, so it is essentially useless in the
# modern point of view. To use a standard-conforming version in Solaris,
# one needs to explicitly use /usr/xpg4/bin/awk.
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
# choose the faster mawk if: it's installed && build date >= 20230322 &&
# version >= 1.3.4
local n x y z d
IFS=' .' read -r 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
fi
fi
# Note: macOS awk has a quirk that it stops processing at all when it sees
# any data not following UTF-8 in the input stream when the current LC_CTYPE
# specifies the UTF-8 encoding. To work around this quirk, one needs to
# specify LC_ALL=C to change the current encoding to the plain one.
LC_ALL=C exec "$__fzf_awk" "$@"
}

View File

@@ -31,21 +31,37 @@ if [[ $- =~ i ]]; then
########################################################### ###########################################################
# To redraw line after fzf closes (printf '\e[5n') #----BEGIN shfmt
bind '"\e[0n": redraw-current-line' 2> /dev/null #----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r 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
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__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 "$@"
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then elif [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; }; then
shift shift
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@" fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
else else
@@ -57,13 +73,13 @@ __fzf_comprun() {
__fzf_orig_completion() { __fzf_orig_completion() {
local l comp f cmd local l comp f cmd
while read -r l; do while read -r l; do
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
comp="${BASH_REMATCH[1]}" comp="${BASH_REMATCH[1]}"
f="${BASH_REMATCH[2]}" f="${BASH_REMATCH[2]}"
cmd="${BASH_REMATCH[3]}" cmd="${BASH_REMATCH[3]}"
[[ "$f" = _fzf_* ]] && continue [[ $f == _fzf_* ]] && continue
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}" printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd " __fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
fi fi
fi fi
@@ -99,7 +115,7 @@ _fzf_opts_completion() {
local cur prev opts local cur prev opts
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD - 1]}"
opts=" opts="
+c --no-color +c --no-color
+i --no-ignore-case +i --no-ignore-case
@@ -183,27 +199,27 @@ _fzf_opts_completion() {
case "${prev}" in case "${prev}" in
--scheme) --scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") ) COMPREPLY=($(compgen -W "default path history" -- "$cur"))
return 0 return 0
;; ;;
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") ) COMPREPLY=($(compgen -W "length chunk begin end index" -- "$cur"))
return 0 return 0
;; ;;
--color) --color)
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") ) COMPREPLY=($(compgen -W "dark light 16 bw no" -- "$cur"))
return 0 return 0
;; ;;
--layout) --layout)
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") ) COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
return 0 return 0
;; ;;
--info) --info)
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") ) COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
return 0 return 0
;; ;;
--preview-window) --preview-window)
COMPREPLY=( $(compgen -W " COMPREPLY=($(compgen -W "
default default
hidden hidden
nohidden nohidden
@@ -229,21 +245,21 @@ _fzf_opts_completion() {
border-left border-left
border-right border-right
follow follow
nofollow" -- "$cur") ) nofollow" -- "$cur"))
return 0 return 0
;; ;;
--border) --border)
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") ) COMPREPLY=($(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
return 0 return 0
;; ;;
--border-label-pos|--preview-label-pos) --border-label-pos | --preview-label-pos)
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") ) COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
return 0 return 0
;; ;;
esac esac
if [[ "$cur" =~ ^-|\+ ]]; then if [[ $cur =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") ) COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
return 0 return 0
fi fi
@@ -257,7 +273,7 @@ _fzf_handle_dynamic_completion() {
orig_cmd="$1" orig_cmd="$1"
if __fzf_orig_completion_get_orig_func "$cmd"; then if __fzf_orig_completion_get_orig_func "$cmd"; then
"$REPLY" "$@" "$REPLY" "$@"
elif [[ -n "${_fzf_completion_loader-}" ]]; then elif [[ -n ${_fzf_completion_loader-} ]]; then
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null) orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
$_fzf_completion_loader "$@" $_fzf_completion_loader "$@"
ret=$? ret=$?
@@ -271,7 +287,7 @@ _fzf_handle_dynamic_completion() {
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" && __fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
orig_complete=$REPLY orig_complete=$REPLY
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
eval "${orig_complete/ -F / -o nospace -F }" eval "${orig_complete/ -F / -o nospace -F }"
else else
eval "$orig_complete" eval "$orig_complete"
@@ -291,18 +307,18 @@ __fzf_generic_path_completion() {
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}" [[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval "base=$base" 2> /dev/null || return eval "base=$base" 2> /dev/null || return
dir= dir=
[[ $base = *"/"* ]] && dir="$base" [[ $base == *"/"* ]] && dir="$base"
while true; do while true; do
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then if [[ -z $dir ]] || [[ -d $dir ]]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"/}
leftover=${leftover/#\/} leftover=${leftover/#\//}
[[ -z "$dir" ]] && dir='.' [[ -z $dir ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ $dir != "/" ]] && dir="${dir/%\//}"
matches=$( matches=$(
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2") export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
@@ -322,17 +338,19 @@ __fzf_generic_path_completion() {
done done
) )
matches=${matches% } matches=${matches% }
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches " [[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
if [[ -n "$matches" ]]; then if [[ -n $matches ]]; then
COMPREPLY=( "$matches" ) COMPREPLY=("$matches")
else else
COMPREPLY=( "$cur" ) COMPREPLY=("$cur")
fi fi
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n' printf '\e[5n'
return 0 return 0
fi fi
dir=$(command dirname "$dir") dir=$(command dirname "$dir")
[[ "$dir" =~ /$ ]] || dir="$dir"/ [[ $dir =~ /$ ]] || dir="$dir"/
done done
else else
shift shift
@@ -348,15 +366,15 @@ _fzf_complete() {
args=("$@") args=("$@")
sep= sep=
for i in "${!args[@]}"; do for i in "${!args[@]}"; do
if [[ "${args[$i]}" = -- ]]; then if [[ ${args[$i]} == -- ]]; then
sep=$i sep=$i
break break
fi fi
done done
if [[ -n "$sep" ]]; then if [[ -n $sep ]]; then
str_arg= str_arg=
rest=("${args[@]:$((sep + 1)):${#args[@]}}") rest=("${args[@]:$((sep + 1)):${#args[@]}}")
args=("${args[@]:0:$sep}") args=("${args[@]:0:sep}")
else else
str_arg=$1 str_arg=$1
args=() args=()
@@ -365,25 +383,27 @@ _fzf_complete() {
fi fi
local cur selected trigger cmd post local cur selected trigger cmd post
post="$(caller 0 | command awk '{print $2}')_post" post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post='command cat' type -t "$post" > /dev/null 2>&1 || post='command cat'
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cmd="${COMP_WORDS[0]}" cmd="${COMP_WORDS[0]}"
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
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=$( selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \ FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ') __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$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")
else else
COMPREPLY=("$cur") COMPREPLY=("$cur")
fi fi
bind '"\e[0n": redraw-current-line' 2> /dev/null
printf '\e[5n' printf '\e[5n'
return 0 return 0
else else
@@ -443,7 +463,7 @@ _fzf_proc_completion() {
} }
_fzf_proc_completion_post() { _fzf_proc_completion_post() {
command awk '{print $2}' __fzf_exec_awk '{print $2}'
} }
# To use custom hostname lists, override __fzf_list_hosts. # To use custom hostname lists, override __fzf_list_hosts.
@@ -460,10 +480,54 @@ _fzf_proc_completion_post() {
# } # }
if ! declare -F __fzf_list_hosts > /dev/null; then if ! declare -F __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() { __fzf_list_hosts() {
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \ command sort -u \
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \ <(
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') | # Note: To make the pathname expansion of "~/.ssh/config.d/*" work
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u # properly, we need to adjust the related shell options. We need to
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
# avoid matching unwanted files. We need to unset "failglob" to avoid
# outputting the error messages to the terminal when no matching is
# found. We need to set "nullglob" to avoid attempting to read the
# literal filename '~/.ssh/config.d/*' when no matching is found.
set +f
GLOBIGNORE=
shopt -u dotglob nocaseglob failglob
shopt -s nullglob
__fzf_exec_awk '
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
sub(/#.*/, "")
for (i = 1; i <= NF; i++)
if ($i !~ /[*?%]/)
print $i
}
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
) \
<(
__fzf_exec_awk -F ',' '
match($0, /^[][a-zA-Z0-9.,:-]+/) {
$0 = substr($0, 1, RLENGTH)
gsub(/[][]|:[^,]*/, "")
for (i = 1; i <= NF; i++)
print $i
}
' ~/.ssh/known_hosts 2> /dev/null
) \
<(
__fzf_exec_awk '
{
sub(/#.*/, "")
for (i = 2; i <= NF; i++)
if ($i != "0.0.0.0")
print $i
}
' /etc/hosts 2> /dev/null
)
} }
fi fi
@@ -478,13 +542,13 @@ _fzf_host_completion() {
# > and the third argument ($3) is the word preceding the word being completed on the current command line. # > and the third argument ($3) is the word preceding the word being completed on the current command line.
_fzf_complete_ssh() { _fzf_complete_ssh() {
case $3 in case $3 in
-i|-F|-E) -i | -F | -E)
_fzf_path_completion "$@" _fzf_path_completion "$@"
;; ;;
*) *)
local user= local user=
[[ "$2" =~ '@' ]] && user="${2%%@*}@" [[ $2 =~ '@' ]] && user="${2%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}') _fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
;; ;;
esac esac
} }
@@ -572,7 +636,7 @@ __fzf_defc() {
if __fzf_orig_completion_instantiate "$cmd" "$func"; then if __fzf_orig_completion_instantiate "$cmd" "$func"; then
eval "$REPLY" eval "$REPLY"
else else
complete -F "$func" $opts "$cmd" eval "complete -F \"$func\" $opts \"$cmd\""
fi fi
} }
@@ -621,5 +685,6 @@ _fzf_setup_completion() {
esac esac
done done
} }
#----END shfmt
fi fi

View File

@@ -96,14 +96,32 @@ if [[ -o interactive ]]; then
########################################################### ###########################################################
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo -E "${FZF_DEFAULT_OPTS-} $2" printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r 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
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
@@ -242,11 +260,50 @@ _fzf_complete() {
# desired sorting and with any duplicates removed, to standard output. # desired sorting and with any duplicates removed, to standard output.
if ! declare -f __fzf_list_hosts > /dev/null; then if ! declare -f __fzf_list_hosts > /dev/null; then
__fzf_list_hosts() { __fzf_list_hosts() {
setopt localoptions nonomatch command sort -u \
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \ <(
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ # Note: To make the pathname expansion of "~/.ssh/config.d/*" work
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') | # properly, we need to adjust the related shell options. We need to
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u # unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
# when no matching is found.
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
__fzf_exec_awk '
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
sub(/#.*/, "")
for (i = 1; i <= NF; i++)
if ($i !~ /[*?%]/)
print $i
}
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
) \
<(
__fzf_exec_awk -F ',' '
match($0, /^[][a-zA-Z0-9.,:-]+/) {
$0 = substr($0, 1, RLENGTH)
gsub(/[][]|:[^,]*/, "")
for (i = 1; i <= NF; i++)
print $i
}
' ~/.ssh/known_hosts 2> /dev/null
) \
<(
__fzf_exec_awk '
{
sub(/#.*/, "")
for (i = 2; i <= NF; i++)
if ($i != "0.0.0.0")
print $i
}
' /etc/hosts 2> /dev/null
)
} }
fi fi
@@ -266,7 +323,7 @@ _fzf_complete_ssh() {
*) *)
local user local user
[[ $prefix =~ @ ]] && user="${prefix%%@*}@" [[ $prefix =~ @ ]] && user="${prefix%%@*}@"
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}') _fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
;; ;;
esac esac
} }
@@ -324,7 +381,7 @@ _fzf_complete_kill() {
} }
_fzf_complete_kill_post() { _fzf_complete_kill_post() {
awk '{print $2}' __fzf_exec_awk '{print $2}'
} }
fzf-completion() { fzf-completion() {

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS # - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS # - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
@@ -17,14 +18,33 @@ if [[ $- =~ i ]]; then
# Key bindings # Key bindings
# ------------ # ------------
#----BEGIN shfmt
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2" printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r 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
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
__fzf_select__() { __fzf_select__() {
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \ 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=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
@@ -35,14 +55,14 @@ __fzf_select__() {
} }
__fzfcmd() { __fzfcmd() {
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } && [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; } &&
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
} }
fzf-file-widget() { fzf-file-widget() {
local selected="$(__fzf_select__ "$@")" local selected="$(__fzf_select__ "$@")"
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} )) READLINE_POINT=$((READLINE_POINT + ${#selected}))
} }
__fzf_cd__() { __fzf_cd__() {
@@ -62,11 +82,11 @@ if command -v perl > /dev/null; then
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=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output") 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
READLINE_POINT=0x7fffffff READLINE_POINT=0x7fffffff
@@ -74,13 +94,7 @@ 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 script n x y z d local output script
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# 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)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi
[[ $(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 }
@@ -90,12 +104,12 @@ else # awk - fallback for POSIX systems
output=$( output=$(
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' )* __fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE" 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
echo "$READLINE_LINE" echo "$READLINE_LINE"
else else
READLINE_POINT=0x7fffffff READLINE_POINT=0x7fffffff
@@ -110,37 +124,48 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
bind -m vi-insert '"\C-z": emacs-editing-mode' bind -m vi-insert '"\C-z": emacs-editing-mode'
bind -m emacs-standard '"\C-z": vi-editing-mode' bind -m emacs-standard '"\C-z": vi-editing-mode'
if (( BASH_VERSINFO[0] < 4 )); then if ((BASH_VERSINFO[0] < 4)); then
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"' bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"' bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"' bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"' bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"' bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"' bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
fi
else else
# CTRL-T - Paste the selected file path into the command line # CTRL-T - Paste the selected file path into the command line
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
bind -m emacs-standard -x '"\C-t": fzf-file-widget' bind -m emacs-standard -x '"\C-t": fzf-file-widget'
bind -m vi-command -x '"\C-t": fzf-file-widget' bind -m vi-command -x '"\C-t": fzf-file-widget'
bind -m vi-insert -x '"\C-t": fzf-file-widget' bind -m vi-insert -x '"\C-t": fzf-file-widget'
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
fi
bind -m emacs-standard -x '"\C-r": __fzf_history__' bind -m emacs-standard -x '"\C-r": __fzf_history__'
bind -m vi-command -x '"\C-r": __fzf_history__' bind -m vi-command -x '"\C-r": __fzf_history__'
bind -m vi-insert -x '"\C-r": __fzf_history__' bind -m vi-insert -x '"\C-r": __fzf_history__'
fi
fi fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"' bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
bind -m vi-command '"\ec": "\C-z\ec\C-z"' bind -m vi-command '"\ec": "\C-z\ec\C-z"'
bind -m vi-insert '"\ec": "\C-z\ec\C-z"' bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
fi fi
#----END shfmt
fi fi

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS # - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS # - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
@@ -14,12 +15,21 @@
# Key bindings # Key bindings
# ------------ # ------------
# For compatibility with fish versions down to 3.1.2, the script does not use: # The oldest supported fish version is 3.1b1. To maintain compatibility, the
# - The -f/--function switch of command: set # command substitution syntax $(cmd) should never be used, even behind a version
# - The process substitution syntax: $(cmd) # check, otherwise the source command will fail on fish versions older than 3.4.0.
# - Ranges that omit start/end indexes: $var[$start..] $var[..$end] $var[..]
function fzf_key_bindings function fzf_key_bindings
# Check fish version
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
echo "This script requires fish version 3.1b1 or newer." >&2
return 1
else if not type -q fzf
echo "fzf was not found in path." >&2
return 1
end
function __fzf_defaults function __fzf_defaults
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS # $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
@@ -135,23 +145,22 @@ function fzf_key_bindings
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
if set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0) set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
# Remove last token from commandline. and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
commandline -t ''
for i in $result
commandline -it -- $prefix(string escape -- $i)' '
end
end
commandline -f repaint commandline -f repaint
end end
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
set -l fzf_query (commandline | string escape) set -l -- command_line (commandline)
set -l -- current_line (commandline -L)
set -l -- total_lines (count $command_line)
set -l -- fzf_query (string escape -- $command_line[$current_line])
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \ set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \ '--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
"--bind=ctrl-r:toggle-sort --highlight-line $FZF_CTRL_R_OPTS" \ '--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c) '--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
set -lx FZF_DEFAULT_OPTS_FILE set -lx FZF_DEFAULT_OPTS_FILE
@@ -172,9 +181,13 @@ function fzf_key_bindings
test -z "$fish_private_mode"; and builtin history merge test -z "$fish_private_mode"; and builtin history merge
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0) if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
commandline -- (string replace -a -- \n\t \n $result[1]) if test "$total_lines" -eq 1
test (count $result) -gt 1; and for i in $result[2..-1] commandline -- (string replace -a -- \n\t \n $result)
commandline -i -- (string replace -a -- \n\t \n \n$i) else
set -l a (math $current_line - 1)
set -l b (math $current_line + 1)
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
commandline -a -- '' $command_line[$b..-1]
end end
end end
@@ -202,8 +215,13 @@ function fzf_key_bindings
commandline -f repaint commandline -f repaint
end end
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
if test -n "$FZF_CTRL_R_COMMAND"
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
end
bind \cr fzf-history-widget bind \cr fzf-history-widget
bind -M insert \cr fzf-history-widget bind -M insert \cr fzf-history-widget
end
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND" if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
bind \ct fzf-file-widget bind \ct fzf-file-widget

View File

@@ -7,6 +7,7 @@
# - $FZF_TMUX_OPTS # - $FZF_TMUX_OPTS
# - $FZF_CTRL_T_COMMAND # - $FZF_CTRL_T_COMMAND
# - $FZF_CTRL_T_OPTS # - $FZF_CTRL_T_OPTS
# - $FZF_CTRL_R_COMMAND
# - $FZF_CTRL_R_OPTS # - $FZF_CTRL_R_OPTS
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
@@ -38,14 +39,32 @@ fi
{ {
if [[ -o interactive ]]; then if [[ -o interactive ]]; then
#----BEGIN INCLUDE common.sh
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
__fzf_defaults() { __fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo -E "${FZF_DEFAULT_OPTS-} $2" printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
} }
__fzf_exec_awk() {
if [[ -z ${__fzf_awk-} ]]; then
__fzf_awk=awk
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
__fzf_awk=/usr/xpg4/bin/awk
elif command -v mawk > /dev/null 2>&1; then
local n x y z d
IFS=' .' read -r 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
fi
fi
LC_ALL=C exec "$__fzf_awk" "$@"
}
#----END INCLUDE
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fzf_select() { __fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
@@ -114,16 +133,16 @@ fzf-history-widget() {
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" | selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' | perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
else else
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | selected="$(fc -rl 1 | __fzf_exec_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 --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \ FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi fi
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
zle vi-fetch-history -n $MATCH zle vi-fetch-history -n $MATCH
else # selected is a custom query, not from history else # selected is a custom query, not from history
LBUFFER="$selected" LBUFFER="$selected"
@@ -132,10 +151,15 @@ fzf-history-widget() {
zle reset-prompt zle reset-prompt
return $ret return $ret
} }
zle -N fzf-history-widget if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
bindkey -M emacs '^R' fzf-history-widget if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
bindkey -M vicmd '^R' fzf-history-widget echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
bindkey -M viins '^R' fzf-history-widget fi
zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
fi
fi fi
} always { } always {

68
shell/update.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# This script applies the contents of "common.sh" to the other files.
set -e
dir=${0%"${0##*/}"}
update() {
{
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
cat << EOF
# NOTE: Do not directly edit this section, which is copied from "common.sh".
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
# the changes. See code comments in "common.sh" for the implementation details.
EOF
echo
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
sed -n '/^#----END INCLUDE/,$p' "$1"
} > "$1.part"
mv -f "$1.part" "$1"
}
update "$dir/completion.bash"
update "$dir/completion.zsh"
update "$dir/key-bindings.bash"
update "$dir/key-bindings.zsh"
# Check if --check is in ARGV
check=0
rest=()
for arg in "$@"; do
case $arg in
--check) check=1 ;;
*) rest+=("$arg") ;;
esac
done
fmt() {
if ! grep -q "^#----BEGIN shfmt" "$1"; then
if [[ $check == 1 ]]; then
shfmt -d "$1"
return $?
else
shfmt -w "$1"
fi
else
{
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
} > "$1.part"
if [[ $check == 1 ]]; then
diff -q "$1" "$1.part"
ret=$?
rm -f "$1.part"
return $ret
fi
mv -f "$1.part" "$1"
fi
}
for file in "${rest[@]}"; do
fmt "$file" || exit $?
done

View File

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

View File

@@ -25,132 +25,164 @@ func _() {
_ = x[actBackwardDeleteChar-14] _ = x[actBackwardDeleteChar-14]
_ = x[actBackwardDeleteCharEof-15] _ = x[actBackwardDeleteCharEof-15]
_ = x[actBackwardWord-16] _ = x[actBackwardWord-16]
_ = x[actCancel-17] _ = x[actBackwardSubWord-17]
_ = x[actChangeBorderLabel-18] _ = x[actCancel-18]
_ = x[actChangeGhost-19] _ = x[actChangeBorderLabel-19]
_ = x[actChangeHeader-20] _ = x[actChangeGhost-20]
_ = x[actChangeHeaderLabel-21] _ = x[actChangeHeader-21]
_ = x[actChangeInputLabel-22] _ = x[actChangeFooter-22]
_ = x[actChangeListLabel-23] _ = x[actChangeHeaderLabel-23]
_ = x[actChangeMulti-24] _ = x[actChangeFooterLabel-24]
_ = x[actChangeNth-25] _ = x[actChangeInputLabel-25]
_ = x[actChangePointer-26] _ = x[actChangeListLabel-26]
_ = x[actChangePreview-27] _ = x[actChangeMulti-27]
_ = x[actChangePreviewLabel-28] _ = x[actChangeNth-28]
_ = x[actChangePreviewWindow-29] _ = x[actChangePointer-29]
_ = x[actChangePrompt-30] _ = x[actChangePreview-30]
_ = x[actChangeQuery-31] _ = x[actChangePreviewLabel-31]
_ = x[actClearScreen-32] _ = x[actChangePreviewWindow-32]
_ = x[actClearQuery-33] _ = x[actChangePrompt-33]
_ = x[actClearSelection-34] _ = x[actChangeQuery-34]
_ = x[actClose-35] _ = x[actClearScreen-35]
_ = x[actDeleteChar-36] _ = x[actClearQuery-36]
_ = x[actDeleteCharEof-37] _ = x[actClearSelection-37]
_ = x[actEndOfLine-38] _ = x[actClose-38]
_ = x[actFatal-39] _ = x[actDeleteChar-39]
_ = x[actForwardChar-40] _ = x[actDeleteCharEof-40]
_ = x[actForwardWord-41] _ = x[actEndOfLine-41]
_ = x[actKillLine-42] _ = x[actFatal-42]
_ = x[actKillWord-43] _ = x[actForwardChar-43]
_ = x[actUnixLineDiscard-44] _ = x[actForwardWord-44]
_ = x[actUnixWordRubout-45] _ = x[actForwardSubWord-45]
_ = x[actYank-46] _ = x[actKillLine-46]
_ = x[actBackwardKillWord-47] _ = x[actKillWord-47]
_ = x[actSelectAll-48] _ = x[actKillSubWord-48]
_ = x[actDeselectAll-49] _ = x[actUnixLineDiscard-49]
_ = x[actToggle-50] _ = x[actUnixWordRubout-50]
_ = x[actToggleSearch-51] _ = x[actYank-51]
_ = x[actToggleAll-52] _ = x[actBackwardKillWord-52]
_ = x[actToggleDown-53] _ = x[actBackwardKillSubWord-53]
_ = x[actToggleUp-54] _ = x[actSelectAll-54]
_ = x[actToggleIn-55] _ = x[actDeselectAll-55]
_ = x[actToggleOut-56] _ = x[actToggle-56]
_ = x[actToggleTrack-57] _ = x[actToggleSearch-57]
_ = x[actToggleTrackCurrent-58] _ = x[actToggleAll-58]
_ = x[actToggleHeader-59] _ = x[actToggleDown-59]
_ = x[actToggleWrap-60] _ = x[actToggleUp-60]
_ = x[actToggleMultiLine-61] _ = x[actToggleIn-61]
_ = x[actToggleHscroll-62] _ = x[actToggleOut-62]
_ = x[actTrackCurrent-63] _ = x[actToggleTrack-63]
_ = x[actToggleInput-64] _ = x[actToggleTrackCurrent-64]
_ = x[actHideInput-65] _ = x[actToggleHeader-65]
_ = x[actShowInput-66] _ = x[actToggleWrap-66]
_ = x[actUntrackCurrent-67] _ = x[actToggleMultiLine-67]
_ = x[actDown-68] _ = x[actToggleHscroll-68]
_ = x[actUp-69] _ = x[actToggleRaw-69]
_ = x[actPageUp-70] _ = x[actEnableRaw-70]
_ = x[actPageDown-71] _ = x[actDisableRaw-71]
_ = x[actPosition-72] _ = x[actTrackCurrent-72]
_ = x[actHalfPageUp-73] _ = x[actToggleInput-73]
_ = x[actHalfPageDown-74] _ = x[actHideInput-74]
_ = x[actOffsetUp-75] _ = x[actShowInput-75]
_ = x[actOffsetDown-76] _ = x[actUntrackCurrent-76]
_ = x[actOffsetMiddle-77] _ = x[actDown-77]
_ = x[actJump-78] _ = x[actDownMatch-78]
_ = x[actJumpAccept-79] _ = x[actUp-79]
_ = x[actPrintQuery-80] _ = x[actUpMatch-80]
_ = x[actRefreshPreview-81] _ = x[actPageUp-81]
_ = x[actReplaceQuery-82] _ = x[actPageDown-82]
_ = x[actToggleSort-83] _ = x[actPosition-83]
_ = x[actShowPreview-84] _ = x[actHalfPageUp-84]
_ = x[actHidePreview-85] _ = x[actHalfPageDown-85]
_ = x[actTogglePreview-86] _ = x[actOffsetUp-86]
_ = x[actTogglePreviewWrap-87] _ = x[actOffsetDown-87]
_ = x[actTransform-88] _ = x[actOffsetMiddle-88]
_ = x[actTransformBorderLabel-89] _ = x[actJump-89]
_ = x[actTransformGhost-90] _ = x[actJumpAccept-90]
_ = x[actTransformHeader-91] _ = x[actPrintQuery-91]
_ = x[actTransformHeaderLabel-92] _ = x[actRefreshPreview-92]
_ = x[actTransformInputLabel-93] _ = x[actReplaceQuery-93]
_ = x[actTransformListLabel-94] _ = x[actToggleSort-94]
_ = x[actTransformNth-95] _ = x[actShowPreview-95]
_ = x[actTransformPointer-96] _ = x[actHidePreview-96]
_ = x[actTransformPreviewLabel-97] _ = x[actTogglePreview-97]
_ = x[actTransformPrompt-98] _ = x[actTogglePreviewWrap-98]
_ = x[actTransformQuery-99] _ = x[actTransform-99]
_ = x[actTransformSearch-100] _ = x[actTransformBorderLabel-100]
_ = x[actSearch-101] _ = x[actTransformGhost-101]
_ = x[actPreview-102] _ = x[actTransformHeader-102]
_ = x[actPreviewTop-103] _ = x[actTransformFooter-103]
_ = x[actPreviewBottom-104] _ = x[actTransformHeaderLabel-104]
_ = x[actPreviewUp-105] _ = x[actTransformFooterLabel-105]
_ = x[actPreviewDown-106] _ = x[actTransformInputLabel-106]
_ = x[actPreviewPageUp-107] _ = x[actTransformListLabel-107]
_ = x[actPreviewPageDown-108] _ = x[actTransformNth-108]
_ = x[actPreviewHalfPageUp-109] _ = x[actTransformPointer-109]
_ = x[actPreviewHalfPageDown-110] _ = x[actTransformPreviewLabel-110]
_ = x[actPrevHistory-111] _ = x[actTransformPrompt-111]
_ = x[actPrevSelected-112] _ = x[actTransformQuery-112]
_ = x[actPrint-113] _ = x[actTransformSearch-113]
_ = x[actPut-114] _ = x[actTrigger-114]
_ = x[actNextHistory-115] _ = x[actBgTransform-115]
_ = x[actNextSelected-116] _ = x[actBgTransformBorderLabel-116]
_ = x[actExecute-117] _ = x[actBgTransformGhost-117]
_ = x[actExecuteSilent-118] _ = x[actBgTransformHeader-118]
_ = x[actExecuteMulti-119] _ = x[actBgTransformFooter-119]
_ = x[actSigStop-120] _ = x[actBgTransformHeaderLabel-120]
_ = x[actFirst-121] _ = x[actBgTransformFooterLabel-121]
_ = x[actLast-122] _ = x[actBgTransformInputLabel-122]
_ = x[actReload-123] _ = x[actBgTransformListLabel-123]
_ = x[actReloadSync-124] _ = x[actBgTransformNth-124]
_ = x[actDisableSearch-125] _ = x[actBgTransformPointer-125]
_ = x[actEnableSearch-126] _ = x[actBgTransformPreviewLabel-126]
_ = x[actSelect-127] _ = x[actBgTransformPrompt-127]
_ = x[actDeselect-128] _ = x[actBgTransformQuery-128]
_ = x[actUnbind-129] _ = x[actBgTransformSearch-129]
_ = x[actRebind-130] _ = x[actBgCancel-130]
_ = x[actToggleBind-131] _ = x[actSearch-131]
_ = x[actBecome-132] _ = x[actPreview-132]
_ = x[actShowHeader-133] _ = x[actPreviewTop-133]
_ = x[actHideHeader-134] _ = x[actPreviewBottom-134]
_ = x[actBell-135] _ = x[actPreviewUp-135]
_ = x[actExclude-136] _ = x[actPreviewDown-136]
_ = x[actExcludeMulti-137] _ = x[actPreviewPageUp-137]
_ = x[actPreviewPageDown-138]
_ = x[actPreviewHalfPageUp-139]
_ = x[actPreviewHalfPageDown-140]
_ = x[actPrevHistory-141]
_ = x[actPrevSelected-142]
_ = x[actPrint-143]
_ = x[actPut-144]
_ = x[actNextHistory-145]
_ = x[actNextSelected-146]
_ = x[actExecute-147]
_ = x[actExecuteSilent-148]
_ = x[actExecuteMulti-149]
_ = x[actSigStop-150]
_ = x[actBest-151]
_ = x[actFirst-152]
_ = x[actLast-153]
_ = x[actReload-154]
_ = x[actReloadSync-155]
_ = x[actDisableSearch-156]
_ = x[actEnableSearch-157]
_ = x[actSelect-158]
_ = x[actDeselect-159]
_ = x[actUnbind-160]
_ = x[actRebind-161]
_ = x[actToggleBind-162]
_ = x[actBecome-163]
_ = x[actShowHeader-164]
_ = x[actHideHeader-165]
_ = x[actBell-166]
_ = x[actExclude-167]
_ = x[actExcludeMulti-168]
_ = x[actAsync-169]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti" const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 318, 337, 355, 369, 381, 397, 413, 434, 456, 471, 485, 499, 512, 529, 537, 550, 566, 578, 586, 600, 614, 625, 636, 654, 671, 678, 697, 709, 723, 732, 747, 759, 772, 783, 794, 806, 820, 841, 856, 869, 887, 903, 918, 932, 944, 956, 973, 980, 985, 994, 1005, 1016, 1029, 1044, 1055, 1068, 1083, 1090, 1103, 1116, 1133, 1148, 1161, 1175, 1189, 1205, 1225, 1237, 1260, 1277, 1295, 1318, 1340, 1361, 1376, 1395, 1419, 1437, 1454, 1472, 1481, 1491, 1504, 1520, 1532, 1546, 1562, 1580, 1600, 1622, 1636, 1651, 1659, 1665, 1679, 1694, 1704, 1720, 1735, 1745, 1753, 1760, 1769, 1782, 1798, 1813, 1822, 1833, 1842, 1851, 1864, 1873, 1886, 1899, 1906, 1916, 1931} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1402, 1425, 1442, 1460, 1478, 1501, 1524, 1546, 1567, 1582, 1601, 1625, 1643, 1660, 1678, 1688, 1702, 1727, 1746, 1766, 1786, 1811, 1836, 1860, 1883, 1900, 1921, 1947, 1967, 1986, 2006, 2017, 2026, 2036, 2049, 2065, 2077, 2091, 2107, 2125, 2145, 2167, 2181, 2196, 2204, 2210, 2224, 2239, 2249, 2265, 2280, 2290, 2297, 2305, 2312, 2321, 2334, 2350, 2365, 2374, 2385, 2394, 2403, 2416, 2425, 2438, 2451, 2458, 2468, 2483, 2491}
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) {

View File

@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
} }
func normalizeRune(r rune) rune { func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 { if r < 0x00C0 || r > 0xFF61 {
return r return r
} }
@@ -827,7 +827,7 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
// For simplicity, only look at the bonus at the first character position // For simplicity, only look at the bonus at the first character position
pidx := 0 pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1) bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
for index := 0; index < lenRunes; index++ { for index := 0; index < lenRunes; index++ {
index_ := indexAt(index, lenRunes, forward) index_ := indexAt(index, lenRunes, forward)
char := text.Get(index_) char := text.Get(index_)
@@ -849,7 +849,16 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
bonus = bonusAt(text, index_) bonus = bonusAt(text, index_)
} }
if boundaryCheck { if boundaryCheck {
ok = bonus >= bonusBoundary if forward && pidx_ == 0 {
bbonus = bonus
} else if !forward && pidx_ == lenPattern-1 {
if index_ < lenRunes-1 {
bbonus = bonusAt(text, index_+1)
} else {
bbonus = bonusBoundaryWhite
}
}
ok = bbonus >= bonusBoundary
if ok && pidx_ == 0 { if ok && pidx_ == 0 {
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
} }

View File

@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
'ử': 'u', 'ử': 'u',
'ữ': 'u', 'ữ': 'u',
'ự': 'u', 'ự': 'u',
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
0xFF01: '!', // Fullwidth exclamation
0xFF02: '"', // Fullwidth quotation mark
0xFF03: '#', // Fullwidth number sign
0xFF04: '$', // Fullwidth dollar sign
0xFF05: '%', // Fullwidth percent
0xFF06: '&', // Fullwidth ampersand
0xFF07: '\'', // Fullwidth apostrophe
0xFF08: '(', // Fullwidth left parenthesis
0xFF09: ')', // Fullwidth right parenthesis
0xFF0A: '*', // Fullwidth asterisk
0xFF0B: '+', // Fullwidth plus
0xFF0C: ',', // Fullwidth comma
0xFF0D: '-', // Fullwidth hyphen-minus
0xFF0E: '.', // Fullwidth period
0xFF0F: '/', // Fullwidth slash
0xFF10: '0',
0xFF11: '1',
0xFF12: '2',
0xFF13: '3',
0xFF14: '4',
0xFF15: '5',
0xFF16: '6',
0xFF17: '7',
0xFF18: '8',
0xFF19: '9',
0xFF1A: ':', // Fullwidth colon
0xFF1B: ';', // Fullwidth semicolon
0xFF1C: '<', // Fullwidth less-than
0xFF1D: '=', // Fullwidth equal
0xFF1E: '>', // Fullwidth greater-than
0xFF1F: '?', // Fullwidth question mark
0xFF20: '@', // Fullwidth at sign
0xFF21: 'A',
0xFF22: 'B',
0xFF23: 'C',
0xFF24: 'D',
0xFF25: 'E',
0xFF26: 'F',
0xFF27: 'G',
0xFF28: 'H',
0xFF29: 'I',
0xFF2A: 'J',
0xFF2B: 'K',
0xFF2C: 'L',
0xFF2D: 'M',
0xFF2E: 'N',
0xFF2F: 'O',
0xFF30: 'P',
0xFF31: 'Q',
0xFF32: 'R',
0xFF33: 'S',
0xFF34: 'T',
0xFF35: 'U',
0xFF36: 'V',
0xFF37: 'W',
0xFF38: 'X',
0xFF39: 'Y',
0xFF3A: 'Z',
0xFF3B: '[', // Fullwidth left bracket
0xFF3C: '\\', // Fullwidth backslash
0xFF3D: ']', // Fullwidth right bracket
0xFF3E: '^', // Fullwidth circumflex
0xFF3F: '_', // Fullwidth underscore
0xFF40: '`', // Fullwidth grave accent
0xFF41: 'a',
0xFF42: 'b',
0xFF43: 'c',
0xFF44: 'd',
0xFF45: 'e',
0xFF46: 'f',
0xFF47: 'g',
0xFF48: 'h',
0xFF49: 'i',
0xFF4A: 'j',
0xFF4B: 'k',
0xFF4C: 'l',
0xFF4D: 'm',
0xFF4E: 'n',
0xFF4F: 'o',
0xFF50: 'p',
0xFF51: 'q',
0xFF52: 'r',
0xFF53: 's',
0xFF54: 't',
0xFF55: 'u',
0xFF56: 'v',
0xFF57: 'w',
0xFF58: 'x',
0xFF59: 'y',
0xFF5A: 'z',
0xFF5B: '{', // Fullwidth left brace
0xFF5C: '|', // Fullwidth vertical bar
0xFF5D: '}', // Fullwidth right brace
0xFF5E: '~', // Fullwidth tilde
0xFF61: '.', // Halfwidth ideographic full stop
} }
// NormalizeRunes normalizes latin script letters // NormalizeRunes normalizes latin script letters
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes)) ret := make([]rune, len(runes))
copy(ret, runes) copy(ret, runes)
for idx, r := range runes { for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 { if r < 0x00C0 || r > 0xFF61 {
continue continue
} }
n := normalized[r] n := normalized[r]

View File

@@ -156,13 +156,13 @@ func isCtrlSeqStart(c uint8) bool {
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to // nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used): // calling FindStringIndex() on the below regex (which was originally used):
// //
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)" // "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
func nextAnsiEscapeSequence(s string) (int, int) { func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences // fast check for ANSI escape sequences
i := 0 i := 0
for ; i < len(s); i++ { for ; i < len(s); i++ {
switch s[i] { switch s[i] {
case '\x0e', '\x0f', '\x1b', '\x08': case '\x0e', '\x0f', '\x1b', '\x08', '\n':
// We ignore the fact that '\x08' cannot be the first char // We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of // in the string and be an escape sequence for the sake of
// speed and simplicity. // speed and simplicity.
@@ -174,6 +174,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
Loop: Loop:
for ; i < len(s); i++ { for ; i < len(s); i++ {
switch s[i] { switch s[i] {
case '\n':
// match: `\n`
return i, i + 1
case '\x08': case '\x08':
// backtrack to match: `.\x08` // backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' { if i > 0 && s[i-1] != '\n' {
@@ -265,13 +268,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(prev) output.WriteString(prev)
} }
newState := interpretCode(str[start:idx], state) code := str[start:idx]
if !newState.equals(state) { newState := interpretCode(code, state)
if code == "\n" || !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount) (&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
} }
if code == "\n" {
output.WriteRune('\n')
runeCount++
// Full-background marker
if newState.lbg >= 0 {
marker := newState
marker.attr |= tui.FullBg
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
marker,
})
// Reset the full-line background color
newState.lbg = -1
}
}
if newState.colored() { if newState.colored() {
// Append new offset // Append new offset
if pstate == nil { if pstate == nil {
@@ -349,6 +369,13 @@ func parseAnsiCode(s string) (int, string) {
} }
func interpretCode(ansiCode string, prevState *ansiState) ansiState { func interpretCode(ansiCode string, prevState *ansiState) ansiState {
if ansiCode == "\n" {
if prevState != nil {
return *prevState
}
return ansiState{-1, -1, 0, -1, nil}
}
var state ansiState var state ansiState
if prevState == nil { if prevState == nil {
state = ansiState{-1, -1, 0, -1, nil} state = ansiState{-1, -1, 0, -1, nil}
@@ -356,7 +383,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url} state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
} }
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") { if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
state.lbg = prevState.bg state.lbg = prevState.bg
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) { } else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
stLen := 2 stLen := 2
@@ -375,10 +402,14 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
return state return state
} }
if len(ansiCode) <= 3 { reset := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.attr = 0 state.attr = 0
}
if len(ansiCode) <= 3 {
reset()
return state return state
} }
ansiCode = ansiCode[2 : len(ansiCode)-1] ansiCode = ansiCode[2 : len(ansiCode)-1]
@@ -432,9 +463,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
case 29: case 29:
state.attr = state.attr &^ tui.StrikeThrough state.attr = state.attr &^ tui.StrikeThrough
case 0: case 0:
state.fg = -1 reset()
state.bg = -1
state.attr = 0
state256 = 0 state256 = 0
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
@@ -474,9 +503,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
// Empty sequence: reset // Empty sequence: reset
if count == 0 { if count == 0 {
state.fg = -1 reset()
state.bg = -1
state.attr = 0
} }
if state256 > 0 { if state256 > 0 {

View File

@@ -22,7 +22,7 @@ import (
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php) // (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html // - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html // - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)") var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
func testParserReference(t testing.TB, str string) { func testParserReference(t testing.TB, str string) {
t.Helper() t.Helper()

View File

@@ -41,6 +41,13 @@ func (c *Chunk) IsFull() bool {
return c.count == chunkSize return c.count == chunkSize
} }
func (c *Chunk) lastIndex(minValue int32) int32 {
if c.count == 0 {
return minValue
}
return c.items[c.count-1].Index() + 1 // Exclusive
}
func (cl *ChunkList) lastChunk() *Chunk { func (cl *ChunkList) lastChunk() *Chunk {
return cl.chunks[len(cl.chunks)-1] return cl.chunks[len(cl.chunks)-1]
} }

View File

@@ -52,7 +52,7 @@ func TestChunkList(t *testing.T) {
// Add more data // Add more data
for i := 0; i < chunkSize*2; i++ { for i := 0; i < chunkSize*2; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i))) cl.Push(fmt.Appendf(nil, "item %d", i))
} }
// Previous snapshot should remain the same // Previous snapshot should remain the same
@@ -86,7 +86,7 @@ func TestChunkListTail(t *testing.T) {
}) })
total := chunkSize*2 + chunkSize/2 total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ { for i := 0; i < total; i++ {
cl.Push([]byte(fmt.Sprintf("item %d", i))) cl.Push(fmt.Appendf(nil, "item %d", i))
} }
snapshot, count, changed := cl.Snapshot(0) snapshot, count, changed := cl.Snapshot(0)

View File

@@ -29,6 +29,10 @@ const (
maxPatternLength = 1000 maxPatternLength = 1000
maxMulti = math.MaxInt32 maxMulti = math.MaxInt32
// Background processes
maxBgProcesses = 30
maxBgProcessesPerAction = 3
// Matcher // Matcher
numPartitionsMultiplier = 8 numPartitionsMultiplier = 8
maxPartitions = 32 maxPartitions = 32

View File

@@ -2,10 +2,12 @@
package fzf package fzf
import ( import (
"maps"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -39,7 +41,7 @@ func (r revision) compatible(other revision) bool {
// Run starts fzf // Run starts fzf
func Run(opts *Options) (int, error) { func Run(opts *Options) (int, error) {
if opts.Filter == nil { if opts.Filter == nil {
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && len(os.Getenv("TMUX_PANE")) > 0 && opts.Tmux.index >= opts.Height.index { if opts.useTmux() {
return runTmux(os.Args, opts) return runTmux(os.Args, opts)
} }
@@ -74,21 +76,25 @@ func Run(opts *Options) (int, error) {
var lineAnsiState, prevLineAnsiState *ansiState var lineAnsiState, prevLineAnsiState *ansiState
if opts.Ansi { if opts.Ansi {
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(byteString(data), lineAnsiState, nil) trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState lineAnsiState = newState
// Full line background is found. Add a special marker.
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
marker := (*offsets)[len(*offsets)-1]
marker.offset[0] = marker.offset[1]
marker.color.bg = newState.lbg
marker.color.attr = marker.color.attr | tui.FullBg
newOffsets := append(*offsets, marker)
offsets = &newOffsets
// Reset the full-line background color
lineAnsiState.lbg = -1
}
return util.ToChars(stringBytes(trimmed)), offsets return util.ToChars(stringBytes(trimmed)), offsets
} }
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(stringBytes(trimmed)), nil
}
}
} }
// Chunk list // Chunk list
@@ -112,7 +118,7 @@ func Run(opts *Options) (int, error) {
nthTransformer := opts.WithNth(opts.Delimiter) nthTransformer := opts.WithNth(opts.Delimiter)
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool { chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
tokens := Tokenize(byteString(data), opts.Delimiter) tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 { if opts.Ansi && len(tokens) > 1 {
var ansiState *ansiState var ansiState *ansiState
if prevLineAnsiState != nil { if prevLineAnsiState != nil {
ansiStateDup := *prevLineAnsiState ansiStateDup := *prevLineAnsiState
@@ -135,7 +141,17 @@ func Run(opts *Options) (int, error) {
return false return false
} }
item.text, item.colors = ansiProcessor(stringBytes(transformed)) item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
// We should not trim trailing whitespaces with background colors
var maxColorOffset int32
if item.colors != nil {
for _, ansi := range *item.colors {
if ansi.color.bg >= 0 {
maxColorOffset = util.Max32(maxColorOffset, ansi.offset[1])
}
}
}
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
item.text.Index = itemIndex item.text.Index = itemIndex
item.origText = &data item.origText = &data
itemIndex++ itemIndex++
@@ -210,10 +226,7 @@ func Run(opts *Options) (int, error) {
} }
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
denyMutex.Lock() denyMutex.Lock()
denylistCopy := make(map[int32]struct{}) denylistCopy := maps.Clone(denylist)
for k, v := range denylist {
denylistCopy[k] = v
}
denyMutex.Unlock() denyMutex.Unlock()
return BuildPattern(cache, patternCache, 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,
@@ -254,11 +267,11 @@ func Run(opts *Options) (int, error) {
// NOTE: Streaming filter is inherently not compatible with --tail // NOTE: Streaming filter is inherently not compatible with --tail
snapshot, _, _ := chunkList.Snapshot(opts.Tail) snapshot, _, _ := chunkList.Snapshot(opts.Tail)
merger, _ := matcher.scan(MatchRequest{ result := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < result.merger.Length(); i++ {
opts.Printer(merger.Get(i).item.AsString(opts.Ansi)) opts.Printer(result.merger.Get(i).item.AsString(opts.Ansi))
found = true found = true
} }
} }
@@ -295,6 +308,7 @@ func Run(opts *Options) (int, error) {
// Event coordination // Event coordination
reading := true reading := true
ticks := 0 ticks := 0
startTick := 0
var nextCommand *commandSpec var nextCommand *commandSpec
var nextEnviron []string var nextEnviron []string
eventBox.Watch(EvtReadNew) eventBox.Watch(EvtReadNew)
@@ -321,6 +335,7 @@ func Run(opts *Options) (int, error) {
clearDenylist() clearDenylist()
} }
reading = true reading = true
startTick = ticks
chunkList.Clear() chunkList.Clear()
itemIndex = 0 itemIndex = 0
inputRevision.bumpMajor() inputRevision.bumpMajor()
@@ -464,12 +479,13 @@ func Run(opts *Options) (int, error) {
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case MatchResult:
merger := val.merger
if deferred { if deferred {
count := val.Length() count := merger.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
determine(val.final) determine(merger.final)
} else if val.final { } else if merger.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery { if opts.PrintQuery {
opts.Printer(opts.Query) opts.Printer(opts.Query)
@@ -487,7 +503,7 @@ func Run(opts *Options) (int, error) {
} }
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
opts.Printer(transformer(val.Get(i).item)) opts.Printer(transformer(merger.Get(i).item))
} }
if count == 0 { if count == 0 {
exitCode = ExitNoMatch exitCode = ExitNoMatch
@@ -495,7 +511,7 @@ func Run(opts *Options) (int, error) {
stop = true stop = true
return return
} }
determine(val.final) determine(merger.final)
} }
} }
terminal.UpdateList(val) terminal.UpdateList(val)
@@ -509,7 +525,7 @@ func Run(opts *Options) (int, error) {
} }
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep, time.Duration(ticks-startTick)*coordinatorDelayStep,
0, coordinatorDelayMax) 0, coordinatorDelayMax)
time.Sleep(dur) time.Sleep(dur)
} }

View File

@@ -19,6 +19,20 @@ type MatchRequest struct {
revision revision revision revision
} }
type MatchResult struct {
merger *Merger
passMerger *Merger
cancelled bool
}
func (mr MatchResult) cacheable() bool {
return mr.merger != nil && mr.merger.cacheable()
}
func (mr MatchResult) final() bool {
return mr.merger != nil && mr.merger.final
}
// Matcher is responsible for performing search // Matcher is responsible for performing search
type Matcher struct { type Matcher struct {
cache *ChunkCache cache *ChunkCache
@@ -29,7 +43,7 @@ type Matcher struct {
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
slab []*util.Slab slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]MatchResult
revision revision revision revision
} }
@@ -51,7 +65,7 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: partitions, partitions: partitions,
slab: make([]*util.Slab, partitions), slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger), mergerCache: make(map[string]MatchResult),
revision: revision} revision: revision}
} }
@@ -85,43 +99,42 @@ func (m *Matcher) Loop() {
cacheCleared := false 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.mergerCache = make(map[string]MatchResult)
m.mergerCache = make(map[string]*Merger)
if !request.revision.compatible(m.revision) { if !request.revision.compatible(m.revision) {
m.cache.Clear() m.cache.Clear()
} }
m.revision = request.revision
cacheCleared = true cacheCleared = true
} }
// Restart search // Restart search
patternString := request.pattern.AsString() patternString := request.pattern.AsString()
var merger *Merger var result MatchResult
cancelled := false
count := CountItems(request.chunks) count := CountItems(request.chunks)
if !cacheCleared { if !cacheCleared {
if count == prevCount { if count == prevCount {
// Look up mergerCache // Look up mergerCache
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final { if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
merger = cached result = cached
} }
} else { } else {
// Invalidate mergerCache // Invalidate mergerCache
prevCount = count prevCount = count
m.mergerCache = make(map[string]*Merger) m.mergerCache = make(map[string]MatchResult)
} }
} }
if merger == nil { if result.merger == nil {
merger, cancelled = m.scan(request) result = m.scan(request)
} }
if !cancelled { if !result.cancelled {
if merger.cacheable() { if result.cacheable() {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = result
} }
merger.final = request.final result.merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger) m.eventBox.Set(EvtSearchFin, result)
} }
} }
} }
@@ -152,19 +165,22 @@ type partialResult struct {
matches []Result matches []Result
} }
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) MatchResult {
startedAt := time.Now() startedAt := time.Now()
numChunks := len(request.chunks) numChunks := len(request.chunks)
if numChunks == 0 { if numChunks == 0 {
return EmptyMerger(request.revision), false m := EmptyMerger(request.revision)
return MatchResult{m, m, false}
} }
pattern := request.pattern pattern := request.pattern
passMerger := PassMerger(&request.chunks, m.tac, request.revision)
if pattern.IsEmpty() { if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac, request.revision), false return MatchResult{passMerger, passMerger, false}
} }
minIndex := request.chunks[0].items[0].Index() minIndex := request.chunks[0].items[0].Index()
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -223,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
if m.reqBox.Peek(reqReset) { if m.reqBox.Peek(reqReset) {
return nil, wait() return MatchResult{nil, nil, wait()}
} }
if time.Since(startedAt) > progressMinDuration { if time.Since(startedAt) > progressMinDuration {
@@ -236,7 +252,8 @@ 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 && request.pattern.sortable, m.tac, request.revision, minIndex), false merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
return MatchResult{merger, passMerger, false}
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search

View File

@@ -4,7 +4,7 @@ import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
func EmptyMerger(revision revision) *Merger { func EmptyMerger(revision revision) *Merger {
return NewMerger(nil, [][]Result{}, false, false, revision, 0) return NewMerger(nil, [][]Result{}, false, false, revision, 0, 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
@@ -22,14 +22,16 @@ type Merger struct {
pass bool pass bool
revision revision revision revision
minIndex int32 minIndex int32
maxIndex 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 revision) *Merger { func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
var minIndex int32 var minIndex, maxIndex int32
if len(*chunks) > 0 { if len(*chunks) > 0 {
minIndex = (*chunks)[0].items[0].Index() minIndex = (*chunks)[0].items[0].Index()
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
} }
mg := Merger{ mg := Merger{
pattern: nil, pattern: nil,
@@ -38,7 +40,8 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
count: 0, count: 0,
pass: true, pass: true,
revision: revision, revision: revision,
minIndex: minIndex} minIndex: minIndex,
maxIndex: maxIndex}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += chunk.count mg.count += chunk.count
@@ -47,7 +50,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
@@ -59,7 +62,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
final: false, final: false,
count: 0, count: 0,
revision: revision, revision: revision,
minIndex: minIndex} minIndex: minIndex,
maxIndex: maxIndex}
for _, list := range mg.lists { for _, list := range mg.lists {
mg.count += len(list) mg.count += len(list)
@@ -137,6 +141,15 @@ func (mg *Merger) Get(idx int) Result {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
} }
func (mg *Merger) ToMap() map[int32]Result {
ret := make(map[int32]Result, mg.count)
for i := 0; i < mg.count; i++ {
result := mg.Get(i)
ret[result.Index()] = result
}
return ret
}
func (mg *Merger) cacheable() bool { func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }

View File

@@ -58,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, revision{}, 0) mg := NewMerger(nil, lists, false, false, revision{}, 0, 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")
@@ -70,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, revision{}, 0) mg := NewMerger(nil, lists, true, false, revision{}, 0, 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++ {
@@ -80,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(nil, lists, true, false, revision{}, 0) mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 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))

View File

@@ -3,6 +3,7 @@ package fzf
import ( import (
"errors" "errors"
"fmt" "fmt"
"maps"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -59,7 +60,7 @@ Usage: fzf [options]
GLOBAL STYLE GLOBAL STYLE
--style=PRESET Apply a style preset [default|minimal|full[:BORDER_STYLE] --style=PRESET Apply a style preset [default|minimal|full[:BORDER_STYLE]
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|base16|bw) and/or custom colors
--no-color Disable colors --no-color Disable colors
--no-bold Do not use bold text --no-bold Do not use bold text
@@ -83,7 +84,7 @@ Usage: fzf [options]
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--border[=STYLE] Draw border around the finder --border[=STYLE] Draw border around the finder
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--border-label=LABEL Label to print on the border --border-label=LABEL Label to print on the border
--border-label-pos=COL Position of the border label --border-label-pos=COL Position of the border label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -97,6 +98,7 @@ Usage: fzf [options]
--wrap Enable line wrap --wrap Enable line wrap
--wrap-sign=STR Indicator for wrapped lines --wrap-sign=STR Indicator for wrapped lines
--no-multi-line Disable multi-line display of items when using --read0 --no-multi-line Disable multi-line display of items when using --read0
--raw Enable raw mode (show non-matching items)
--track Track the current selection when the result is updated --track Track the current selection when the result is updated
--tac Reverse the order of the input --tac Reverse the order of the input
--gap[=N] Render empty lines between each item --gap[=N] Render empty lines between each item
@@ -109,6 +111,8 @@ Usage: fzf [options]
--hscroll-off=COLS Number of screen columns to keep to the right of the --hscroll-off=COLS Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--jump-labels=CHARS Label characters for jump mode --jump-labels=CHARS Label characters for jump mode
--gutter=CHAR Character used for the gutter column (default: '▌')
--gutter-raw=CHAR Character used for the gutter column in raw mode (default: '▖')
--pointer=STR Pointer to the current line (default: '▌' or '>') --pointer=STR Pointer to the current line (default: '▌' or '>')
--marker=STR Multi-select marker (default: '┃' or '>') --marker=STR Multi-select marker (default: '┃' or '>')
--marker-multi-line=STR Multi-select marker for multi-line entries; --marker-multi-line=STR Multi-select marker for multi-line entries;
@@ -140,7 +144,7 @@ Usage: fzf [options]
--filepath-word Make word-wise movements respect path separators --filepath-word Make word-wise movements respect path separators
--input-border[=STYLE] Draw border around the input section --input-border[=STYLE] Draw border around the input section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--input-label=LABEL Label to print on the input border --input-label=LABEL Label to print on the input border
--input-label-pos=COL Position of the input label --input-label-pos=COL Position of the input label
[POSITIVE_INTEGER: columns from left| [POSITIVE_INTEGER: columns from left|
@@ -168,7 +172,7 @@ Usage: fzf [options]
--header-first Print header before the prompt line --header-first Print header before the prompt line
--header-border[=STYLE] Draw border around the header section --header-border[=STYLE] Draw border around the header section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical| [rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|none] (default: rounded) top|bottom|left|right|line|none] (default: rounded)
--header-lines-border[=STYLE] --header-lines-border[=STYLE]
Display header from --header-lines with a separate border. Display header from --header-lines with a separate border.
Pass 'none' to still separate it but without a border. Pass 'none' to still separate it but without a border.
@@ -178,6 +182,17 @@ Usage: fzf [options]
NEGATIVE_INTEGER: columns from right][:bottom] NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center) (default: 0 or center)
FOOTER
--footer=STR String to print as footer
--footer-border[=STYLE] Draw border around the footer section
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: line)
--footer-label=LABEL Label to print on the footer border
--footer-label-pos=COL Position of the footer label
[POSITIVE_INTEGER: columns from left|
NEGATIVE_INTEGER: columns from right][:bottom]
(default: 0 or center)
SCRIPTING SCRIPTING
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
@@ -191,8 +206,10 @@ Usage: fzf [options]
ADVANCED ADVANCED
--with-shell=STR Shell command and flags to start child processes with --with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions via TCP
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
--listen=SOCKET_PATH Start HTTP server to receive actions via Unix domain socket
(Path should end with .sock)
DIRECTORY TRAVERSAL (Only used when $FZF_DEFAULT_COMMAND is not set) DIRECTORY TRAVERSAL (Only used when $FZF_DEFAULT_COMMAND is not set)
--walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden) --walker=OPTS [file][,dir][,follow][,hidden] (default: file,follow,hidden)
@@ -201,8 +218,8 @@ Usage: fzf [options]
(default: .git,node_modules) (default: .git,node_modules)
HISTORY HISTORY
--history=FILE History file --history=FILE File to store fzf search history (*not* shell command history)
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of entries to keep in the file (default: 1000)
SHELL INTEGRATION SHELL INTEGRATION
--bash Print script to set up Bash shell integration --bash Print script to set up Bash shell integration
@@ -549,6 +566,7 @@ type Options struct {
AcceptNth func(Delimiter) func([]Token, int32) string AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Raw bool
Track trackOption Track trackOption
Tac bool Tac bool
Tail int Tail int
@@ -556,6 +574,7 @@ type Options struct {
Multi int Multi int
Ansi bool Ansi bool
Mouse bool Mouse bool
BaseTheme *tui.ColorTheme
Theme *tui.ColorTheme Theme *tui.ColorTheme
Black bool Black bool
Bold bool Bold bool
@@ -579,6 +598,8 @@ type Options struct {
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
Gutter *string
GutterRaw *string
Pointer *string Pointer *string
Marker *string Marker *string
MarkerMulti *[3]string MarkerMulti *[3]string
@@ -599,6 +620,7 @@ type Options struct {
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool HeaderFirst bool
Footer []string
Gap int Gap int
GapLine *string GapLine *string
Ellipsis *string Ellipsis *string
@@ -610,8 +632,10 @@ type Options struct {
InputBorderShape tui.BorderShape InputBorderShape tui.BorderShape
HeaderBorderShape tui.BorderShape HeaderBorderShape tui.BorderShape
HeaderLinesShape tui.BorderShape HeaderLinesShape tui.BorderShape
FooterBorderShape tui.BorderShape
InputLabel labelOpts InputLabel labelOpts
HeaderLabel labelOpts HeaderLabel labelOpts
FooterLabel labelOpts
BorderLabel labelOpts BorderLabel labelOpts
ListLabel labelOpts ListLabel labelOpts
PreviewLabel labelOpts PreviewLabel labelOpts
@@ -651,9 +675,9 @@ func defaultPreviewOpts(command string) previewOpts {
func defaultOptions() *Options { func defaultOptions() *Options {
var theme *tui.ColorTheme var theme *tui.ColorTheme
if os.Getenv("NO_COLOR") != "" { if os.Getenv("NO_COLOR") != "" {
theme = tui.NoColorTheme() theme = tui.NoColorTheme
} else { } else {
theme = tui.EmptyTheme() theme = tui.EmptyTheme
} }
return &Options{ return &Options{
@@ -696,6 +720,8 @@ func defaultOptions() *Options {
Separator: nil, Separator: nil,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Gutter: nil,
GutterRaw: nil,
Pointer: nil, Pointer: nil,
Marker: nil, Marker: nil,
MarkerMulti: nil, MarkerMulti: nil,
@@ -716,6 +742,7 @@ func defaultOptions() *Options {
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false, HeaderFirst: false,
Footer: make([]string, 0),
Gap: 0, Gap: 0,
Ellipsis: nil, Ellipsis: nil,
Scrollbar: nil, Scrollbar: nil,
@@ -880,12 +907,9 @@ func parseAlgo(str string) (algo.Algo, error) {
return nil, errors.New("invalid algorithm (expected: v1 or v2)") return nil, errors.New("invalid algorithm (expected: v1 or v2)")
} }
func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) { func parseBorder(str string, optional bool) (tui.BorderShape, error) {
switch str { switch str {
case "line": case "line":
if !allowLine {
return tui.BorderNone, errors.New("'line' is only allowed for preview border")
}
return tui.BorderLine, nil return tui.BorderLine, nil
case "rounded": case "rounded":
return tui.BorderRounded, nil return tui.BorderRounded, nil
@@ -920,15 +944,12 @@ func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, er
return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)") return tui.BorderNone, errors.New("invalid border style (expected: rounded|sharp|bold|block|thinblock|double|horizontal|vertical|top|bottom|left|right|none)")
} }
func parseKeyChords(str string, message string) (map[tui.Event]string, error) { func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Event, error) {
return parseKeyChordsImpl(str, message)
}
func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error) {
if len(str) == 0 { if len(str) == 0 {
return nil, errors.New(message) return nil, nil, errors.New(message)
} }
list := []tui.Event{}
str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma})) str = regexp.MustCompile("(?i)(alt-),").ReplaceAllString(str, "$1"+string([]rune{escapedComma}))
tokens := strings.Split(str, ",") tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") { if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Contains(str, ",,,") {
@@ -944,6 +965,7 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
add := func(e tui.EventType) { add := func(e tui.EventType) {
chords[e.AsEvent()] = key chords[e.AsEvent()] = key
list = append(list, e.AsEvent())
} }
switch lkey { switch lkey {
case "up": case "up":
@@ -957,13 +979,13 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
case "enter", "return": case "enter", "return":
add(tui.Enter) add(tui.Enter)
case "space": case "space":
chords[tui.Key(' ')] = key evt := tui.Key(' ')
chords[evt] = key
list = append(list, evt)
case "backspace", "bspace", "bs": case "backspace", "bspace", "bs":
add(tui.Backspace) add(tui.Backspace)
case "ctrl-space": case "ctrl-space":
add(tui.CtrlSpace) add(tui.CtrlSpace)
case "ctrl-delete":
add(tui.CtrlDelete)
case "ctrl-^", "ctrl-6": case "ctrl-^", "ctrl-6":
add(tui.CtrlCaret) add(tui.CtrlCaret)
case "ctrl-/", "ctrl-_": case "ctrl-/", "ctrl-_":
@@ -996,12 +1018,24 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
add(tui.JumpCancel) add(tui.JumpCancel)
case "click-header": case "click-header":
add(tui.ClickHeader) add(tui.ClickHeader)
case "click-footer":
add(tui.ClickFooter)
case "multi":
add(tui.Multi)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key evt := tui.CtrlAltKey('m')
chords[evt] = key
list = append(list, evt)
case "alt-space": case "alt-space":
chords[tui.AltKey(' ')] = key evt := tui.AltKey(' ')
chords[evt] = key
list = append(list, evt)
case "alt-bs", "alt-bspace", "alt-backspace": case "alt-bs", "alt-bspace", "alt-backspace":
add(tui.AltBackspace) add(tui.AltBackspace)
case "ctrl-bs", "ctrl-bspace", "ctrl-backspace":
add(tui.CtrlBackspace)
case "ctrl-alt-bs", "ctrl-alt-bspace", "ctrl-alt-backspace":
add(tui.CtrlAltBackspace)
case "alt-up": case "alt-up":
add(tui.AltUp) add(tui.AltUp)
case "alt-down": case "alt-down":
@@ -1010,6 +1044,16 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
add(tui.AltLeft) add(tui.AltLeft)
case "alt-right": case "alt-right":
add(tui.AltRight) add(tui.AltRight)
case "alt-home":
add(tui.AltHome)
case "alt-end":
add(tui.AltEnd)
case "alt-delete":
add(tui.AltDelete)
case "alt-page-up":
add(tui.AltPageUp)
case "alt-page-down":
add(tui.AltPageDown)
case "tab": case "tab":
add(tui.Tab) add(tui.Tab)
case "btab", "shift-tab": case "btab", "shift-tab":
@@ -1036,6 +1080,88 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
add(tui.AltShiftLeft) add(tui.AltShiftLeft)
case "alt-shift-right", "shift-alt-right": case "alt-shift-right", "shift-alt-right":
add(tui.AltShiftRight) add(tui.AltShiftRight)
case "alt-shift-home", "shift-alt-home":
add(tui.AltShiftHome)
case "alt-shift-end", "shift-alt-end":
add(tui.AltShiftEnd)
case "alt-shift-delete", "shift-alt-delete":
add(tui.AltShiftDelete)
case "alt-shift-page-up", "shift-alt-page-up":
add(tui.AltShiftPageUp)
case "alt-shift-page-down", "shift-alt-page-down":
add(tui.AltShiftPageDown)
case "ctrl-up":
add(tui.CtrlUp)
case "ctrl-down":
add(tui.CtrlDown)
case "ctrl-right":
add(tui.CtrlRight)
case "ctrl-left":
add(tui.CtrlLeft)
case "ctrl-home":
add(tui.CtrlHome)
case "ctrl-end":
add(tui.CtrlEnd)
case "ctrl-delete":
add(tui.CtrlDelete)
case "ctrl-page-up":
add(tui.CtrlPageUp)
case "ctrl-page-down":
add(tui.CtrlPageDown)
case "ctrl-alt-up", "alt-ctrl-up":
add(tui.CtrlAltUp)
case "ctrl-alt-down", "alt-ctrl-down":
add(tui.CtrlAltDown)
case "ctrl-alt-right", "alt-ctrl-right":
add(tui.CtrlAltRight)
case "ctrl-alt-left", "alt-ctrl-left":
add(tui.CtrlAltLeft)
case "ctrl-alt-home", "alt-ctrl-home":
add(tui.CtrlAltHome)
case "ctrl-alt-end", "alt-ctrl-end":
add(tui.CtrlAltEnd)
case "ctrl-alt-delete", "alt-ctrl-delete":
add(tui.CtrlAltDelete)
case "ctrl-alt-page-up", "alt-ctrl-page-up":
add(tui.CtrlAltPageUp)
case "ctrl-alt-page-down", "alt-ctrl-page-down":
add(tui.CtrlAltPageDown)
case "ctrl-shift-up", "shift-ctrl-up":
add(tui.CtrlShiftUp)
case "ctrl-shift-down", "shift-ctrl-down":
add(tui.CtrlShiftDown)
case "ctrl-shift-right", "shift-ctrl-right":
add(tui.CtrlShiftRight)
case "ctrl-shift-left", "shift-ctrl-left":
add(tui.CtrlShiftLeft)
case "ctrl-shift-home", "shift-ctrl-home":
add(tui.CtrlShiftHome)
case "ctrl-shift-end", "shift-ctrl-end":
add(tui.CtrlShiftEnd)
case "ctrl-shift-delete", "shift-ctrl-delete":
add(tui.CtrlShiftDelete)
case "ctrl-shift-page-up", "shift-ctrl-page-up":
add(tui.CtrlShiftPageUp)
case "ctrl-shift-page-down", "shift-ctrl-page-down":
add(tui.CtrlShiftPageDown)
case "ctrl-alt-shift-up":
add(tui.CtrlAltShiftUp)
case "ctrl-alt-shift-down":
add(tui.CtrlAltShiftDown)
case "ctrl-alt-shift-right":
add(tui.CtrlAltShiftRight)
case "ctrl-alt-shift-left":
add(tui.CtrlAltShiftLeft)
case "ctrl-alt-shift-home":
add(tui.CtrlAltShiftHome)
case "ctrl-alt-shift-end":
add(tui.CtrlAltShiftEnd)
case "ctrl-alt-shift-delete":
add(tui.CtrlAltShiftDelete)
case "ctrl-alt-shift-page-up":
add(tui.CtrlAltShiftPageUp)
case "ctrl-alt-shift-page-down":
add(tui.CtrlAltShiftPageDown)
case "shift-up": case "shift-up":
add(tui.ShiftUp) add(tui.ShiftUp)
case "shift-down": case "shift-down":
@@ -1044,8 +1170,16 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
add(tui.ShiftLeft) add(tui.ShiftLeft)
case "shift-right": case "shift-right":
add(tui.ShiftRight) add(tui.ShiftRight)
case "shift-home":
add(tui.ShiftHome)
case "shift-end":
add(tui.ShiftEnd)
case "shift-delete": case "shift-delete":
add(tui.ShiftDelete) add(tui.ShiftDelete)
case "shift-page-up":
add(tui.ShiftPageUp)
case "shift-page-down":
add(tui.ShiftPageDown)
case "left-click": case "left-click":
add(tui.LeftClick) add(tui.LeftClick)
case "right-click": case "right-click":
@@ -1077,7 +1211,9 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
default: default:
runes := []rune(key) runes := []rune(key)
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
chords[tui.CtrlAltKey(rune(key[9]))] = key evt := tui.CtrlAltKey(rune(key[9]))
chords[evt] = key
list = append(list, evt)
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')) add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a'))
} else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") { } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
@@ -1090,17 +1226,21 @@ func parseKeyChordsImpl(str string, message string) (map[tui.Event]string, error
case escapedPlus: case escapedPlus:
r = '+' r = '+'
} }
chords[tui.AltKey(r)] = key evt := tui.AltKey(r)
chords[evt] = key
list = append(list, evt)
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
add(tui.EventType(tui.F1.Int() + int(key[1]) - '1')) add(tui.EventType(tui.F1.Int() + int(key[1]) - '1'))
} else if len(runes) == 1 { } else if len(runes) == 1 {
chords[tui.Key(runes[0])] = key evt := tui.Key(runes[0])
chords[evt] = key
list = append(list, evt)
} else { } else {
return nil, errors.New("unsupported key: " + key) return nil, list, errors.New("unsupported key: " + key)
} }
} }
} }
return chords, nil return chords, list, nil
} }
func parseScheme(str string) (string, []criterion, error) { func parseScheme(str string) (string, []criterion, error) {
@@ -1180,20 +1320,30 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
return &dupe return &dupe
} }
func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, error) { func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui.ColorTheme, error) {
var err error var err error
var baseTheme *tui.ColorTheme
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
for _, str := range strings.Split(strings.ToLower(str), ",") { comma := regexp.MustCompile(`[\s,]+`)
for _, str := range comma.Split(strings.ToLower(str), -1) {
str = strings.TrimSpace(str)
if len(str) == 0 {
continue
}
switch str { switch str {
case "dark": case "dark":
baseTheme = tui.Dark256
theme = dupeTheme(tui.Dark256) theme = dupeTheme(tui.Dark256)
case "light": case "light":
baseTheme = tui.Light256
theme = dupeTheme(tui.Light256) theme = dupeTheme(tui.Light256)
case "16": case "base16", "16":
baseTheme = tui.Default16
theme = dupeTheme(tui.Default16) theme = dupeTheme(tui.Default16)
case "bw", "no": case "bw", "no":
theme = tui.NoColorTheme() baseTheme = tui.NoColorTheme
theme = dupeTheme(tui.NoColorTheme)
default: default:
fail := func() { fail := func() {
// Let the code proceed to simplify the error handling // Let the code proceed to simplify the error handling
@@ -1218,6 +1368,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
cattr.Attr |= tui.Bold cattr.Attr |= tui.Bold
case "dim": case "dim":
cattr.Attr |= tui.Dim cattr.Attr |= tui.Dim
case "strip":
cattr.Attr |= tui.Strip
case "italic": case "italic":
cattr.Attr |= tui.Italic cattr.Attr |= tui.Italic
case "underline": case "underline":
@@ -1277,6 +1429,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
switch components[0] { switch components[0] {
case "query", "input", "input-fg": case "query", "input", "input-fg":
mergeAttr(&theme.Input) mergeAttr(&theme.Input)
case "ghost":
mergeAttr(&theme.Ghost)
case "disabled": case "disabled":
mergeAttr(&theme.Disabled) mergeAttr(&theme.Disabled)
case "fg": case "fg":
@@ -1295,12 +1449,16 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.Current) mergeAttr(&theme.Current)
case "current-bg", "bg+": case "current-bg", "bg+":
mergeAttr(&theme.DarkBg) mergeAttr(&theme.DarkBg)
case "alt-bg":
mergeAttr(&theme.AltBg)
case "selected-fg": case "selected-fg":
mergeAttr(&theme.SelectedFg) mergeAttr(&theme.SelectedFg)
case "selected-bg": case "selected-bg":
mergeAttr(&theme.SelectedBg) mergeAttr(&theme.SelectedBg)
case "nth": case "nth":
mergeAttr(&theme.Nth) mergeAttr(&theme.Nth)
case "nomatch":
mergeAttr(&theme.Nomatch)
case "gutter": case "gutter":
mergeAttr(&theme.Gutter) mergeAttr(&theme.Gutter)
case "hl": case "hl":
@@ -1339,6 +1497,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.HeaderBorder) mergeAttr(&theme.HeaderBorder)
case "header-label": case "header-label":
mergeAttr(&theme.HeaderLabel) mergeAttr(&theme.HeaderLabel)
case "footer-border":
mergeAttr(&theme.FooterBorder)
case "footer-label":
mergeAttr(&theme.FooterLabel)
case "spinner": case "spinner":
mergeAttr(&theme.Spinner) mergeAttr(&theme.Spinner)
case "info": case "info":
@@ -1351,6 +1513,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
mergeAttr(&theme.Header) mergeAttr(&theme.Header)
case "header-bg": case "header-bg":
mergeAttr(&theme.HeaderBg) mergeAttr(&theme.HeaderBg)
case "footer", "footer-fg":
mergeAttr(&theme.Footer)
case "footer-bg":
mergeAttr(&theme.FooterBg)
case "gap-line": case "gap-line":
mergeAttr(&theme.GapLine) mergeAttr(&theme.GapLine)
default: default:
@@ -1358,7 +1524,7 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, erro
} }
} }
} }
return theme, err return baseTheme, theme, err
} }
func parseWalkerOpts(str string) (walkerOpts, error) { func parseWalkerOpts(str string) (walkerOpts, error) {
@@ -1406,7 +1572,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|(?:border|list|preview|input|header)-label|header|search|nth|pointer|ghost)|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|bg-transform|transform)-(?:query|prompt|(?:border|list|preview|input|header|footer)-label|header|footer|search|nth|pointer|ghost)|bg-transform|transform|change-(?:preview-window|preview|multi)|(?:re|un|toggle-)bind|pos|put|print|search|trigger)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1518,6 +1684,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actBackwardDeleteCharEof) appendAction(actBackwardDeleteCharEof)
case "backward-word": case "backward-word":
appendAction(actBackwardWord) appendAction(actBackwardWord)
case "backward-subword":
appendAction(actBackwardSubWord)
case "clear-screen": case "clear-screen":
appendAction(actClearScreen) appendAction(actClearScreen)
case "delete-char": case "delete-char":
@@ -1532,12 +1700,14 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actCancel) appendAction(actCancel)
case "clear-query": case "clear-query":
appendAction(actClearQuery) appendAction(actClearQuery)
case "clear-selection": case "clear-multi", "clear-selection":
appendAction(actClearSelection) appendAction(actClearSelection)
case "forward-char": case "forward-char":
appendAction(actForwardChar) appendAction(actForwardChar)
case "forward-word": case "forward-word":
appendAction(actForwardWord) appendAction(actForwardWord)
case "forward-subword":
appendAction(actForwardSubWord)
case "jump": case "jump":
appendAction(actJump) appendAction(actJump)
case "jump-accept": case "jump-accept":
@@ -1546,6 +1716,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actKillLine) appendAction(actKillLine)
case "kill-word": case "kill-word":
appendAction(actKillWord) appendAction(actKillWord)
case "kill-subword":
appendAction(actKillSubWord)
case "unix-line-discard", "line-discard": case "unix-line-discard", "line-discard":
appendAction(actUnixLineDiscard) appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout": case "unix-word-rubout", "word-rubout":
@@ -1554,6 +1726,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actYank) appendAction(actYank)
case "backward-kill-word": case "backward-kill-word":
appendAction(actBackwardKillWord) appendAction(actBackwardKillWord)
case "backward-kill-subword":
appendAction(actBackwardKillSubWord)
case "toggle-down": case "toggle-down":
appendAction(actToggle, actDown) appendAction(actToggle, actDown)
case "toggle-up": case "toggle-up":
@@ -1584,6 +1758,12 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleMultiLine) appendAction(actToggleMultiLine)
case "toggle-hscroll": case "toggle-hscroll":
appendAction(actToggleHscroll) appendAction(actToggleHscroll)
case "toggle-raw":
appendAction(actToggleRaw)
case "enable-raw":
appendAction(actEnableRaw)
case "disable-raw":
appendAction(actDisableRaw)
case "show-header": case "show-header":
appendAction(actShowHeader) appendAction(actShowHeader)
case "hide-header": case "hide-header":
@@ -1604,12 +1784,18 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggle) appendAction(actToggle)
case "down": case "down":
appendAction(actDown) appendAction(actDown)
case "down-match":
appendAction(actDownMatch)
case "up": case "up":
appendAction(actUp) appendAction(actUp)
case "up-match":
appendAction(actUpMatch)
case "first", "top": case "first", "top":
appendAction(actFirst) appendAction(actFirst)
case "last": case "last":
appendAction(actLast) appendAction(actLast)
case "best":
appendAction(actBest)
case "page-up": case "page-up":
appendAction(actPageUp) appendAction(actPageUp)
case "page-down": case "page-down":
@@ -1622,9 +1808,9 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actPrevHistory) appendAction(actPrevHistory)
case "next-history": case "next-history":
appendAction(actNextHistory) appendAction(actNextHistory)
case "prev-selected": case "up-selected", "prev-selected":
appendAction(actPrevSelected) appendAction(actPrevSelected)
case "next-selected": case "down-selected", "next-selected":
appendAction(actNextSelected) appendAction(actNextSelected)
case "show-preview": case "show-preview":
appendAction(actShowPreview) appendAction(actShowPreview)
@@ -1674,6 +1860,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actExclude) appendAction(actExclude)
case "exclude-multi": case "exclude-multi":
appendAction(actExcludeMulti) appendAction(actExcludeMulti)
case "bg-cancel":
appendAction(actBgCancel)
default: default:
t := isExecuteAction(specLower) t := isExecuteAction(specLower)
if t == actIgnore { if t == actIgnore {
@@ -1701,7 +1889,7 @@ func parseActionList(masked string, original string, prevActions []*action, putA
} }
switch t { switch t {
case actUnbind, actRebind, actToggleBind: case actUnbind, actRebind, actToggleBind:
if _, err := parseKeyChordsImpl(actionArg, spec[0:offset]+" target required"); err != nil { if _, _, err := parseKeyChords(actionArg, spec[0:offset]+" target required"); err != nil {
return nil, err return nil, err
} }
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -1746,7 +1934,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) error {
} else if len(keyName) == 1 && keyName[0] == escapedPlus { } else if len(keyName) == 1 && keyName[0] == escapedPlus {
key = tui.Key('+') key = tui.Key('+')
} else { } else {
keys, err := parseKeyChordsImpl(keyName, "key name required") keys, _, err := parseKeyChords(keyName, "key name required")
if err != nil { if err != nil {
return err return err
} }
@@ -1791,6 +1979,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-header": case "change-header":
return actChangeHeader return actChangeHeader
case "change-footer":
return actChangeFooter
case "change-list-label": case "change-list-label":
return actChangeListLabel return actChangeListLabel
case "change-border-label": case "change-border-label":
@@ -1801,6 +1991,8 @@ func isExecuteAction(str string) actionType {
return actChangeInputLabel return actChangeInputLabel
case "change-header-label": case "change-header-label":
return actChangeHeaderLabel return actChangeHeaderLabel
case "change-footer-label":
return actChangeFooterLabel
case "change-ghost": case "change-ghost":
return actChangeGhost return actChangeGhost
case "change-pointer": case "change-pointer":
@@ -1841,6 +2033,10 @@ func isExecuteAction(str string) actionType {
return actTransformInputLabel return actTransformInputLabel
case "transform-header-label": case "transform-header-label":
return actTransformHeaderLabel return actTransformHeaderLabel
case "transform-footer-label":
return actTransformFooterLabel
case "transform-footer":
return actTransformFooter
case "transform-header": case "transform-header":
return actTransformHeader return actTransformHeader
case "transform-ghost": case "transform-ghost":
@@ -1855,6 +2051,38 @@ func isExecuteAction(str string) actionType {
return actTransformQuery return actTransformQuery
case "transform-search": case "transform-search":
return actTransformSearch return actTransformSearch
case "bg-transform":
return actBgTransform
case "bg-transform-list-label":
return actBgTransformListLabel
case "bg-transform-border-label":
return actBgTransformBorderLabel
case "bg-transform-preview-label":
return actBgTransformPreviewLabel
case "bg-transform-input-label":
return actBgTransformInputLabel
case "bg-transform-header-label":
return actBgTransformHeaderLabel
case "bg-transform-footer-label":
return actBgTransformFooterLabel
case "bg-transform-footer":
return actBgTransformFooter
case "bg-transform-header":
return actBgTransformHeader
case "bg-transform-ghost":
return actBgTransformGhost
case "bg-transform-nth":
return actBgTransformNth
case "bg-transform-pointer":
return actBgTransformPointer
case "bg-transform-prompt":
return actBgTransformPrompt
case "bg-transform-query":
return actBgTransformQuery
case "bg-transform-search":
return actBgTransformSearch
case "trigger":
return actTrigger
case "search": case "search":
return actSearch return actSearch
} }
@@ -1862,7 +2090,7 @@ func isExecuteAction(str string) actionType {
} }
func parseToggleSort(keymap map[tui.Event][]*action, str string) error { func parseToggleSort(keymap map[tui.Event][]*action, str string) error {
keys, err := parseKeyChords(str, "key name required") keys, _, err := parseKeyChords(str, "key name required")
if err != nil { if err != nil {
return err return err
} }
@@ -2401,13 +2629,11 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
chords, err := parseKeyChords(str, "key names required") chords, _, err := parseKeyChords(str, "key names required")
if err != nil { if err != nil {
return err return err
} }
for k, v := range chords { maps.Copy(opts.Expect, chords)
opts.Expect[k] = v
}
case "--no-expect": case "--no-expect":
opts.Expect = make(map[tui.Event]string) opts.Expect = make(map[tui.Event]string)
case "--enabled", "--no-phony": case "--enabled", "--no-phony":
@@ -2435,11 +2661,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--color": case "--color":
_, spec := optionalNextString() _, spec := optionalNextString()
if len(spec) == 0 { if len(spec) == 0 {
opts.Theme = tui.EmptyTheme() opts.Theme = tui.EmptyTheme
} else { } else {
if opts.Theme, err = parseTheme(opts.Theme, spec); err != nil { var baseTheme *tui.ColorTheme
if baseTheme, opts.Theme, err = parseTheme(opts.Theme, spec); err != nil {
return err return err
} }
if baseTheme != nil {
opts.BaseTheme = baseTheme
}
} }
case "--toggle-sort": case "--toggle-sort":
str, err := nextString("key name required") str, err := nextString("key name required")
@@ -2485,6 +2715,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} }
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--raw":
opts.Raw = true
case "--no-raw":
opts.Raw = false
case "--track": case "--track":
opts.Track = trackEnabled opts.Track = trackEnabled
case "--no-track": case "--no-track":
@@ -2521,7 +2755,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--no-mouse": case "--no-mouse":
opts.Mouse = false opts.Mouse = false
case "+c", "--no-color": case "+c", "--no-color":
opts.Theme = tui.NoColorTheme() opts.BaseTheme = tui.NoColorTheme
opts.Theme = tui.NoColorTheme
case "+2", "--no-256": case "+2", "--no-256":
opts.Theme = tui.Default16 opts.Theme = tui.Default16
case "--black": case "--black":
@@ -2662,6 +2897,20 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err != nil { if err != nil {
return err return err
} }
case "--gutter":
str, err := nextString("gutter character required")
if err != nil {
return err
}
str = firstLine(str)
opts.Gutter = &str
case "--gutter-raw":
str, err := nextString("gutter character for raw mode required")
if err != nil {
return err
}
str = firstLine(str)
opts.GutterRaw = &str
case "--pointer": case "--pointer":
str, err := nextString("pointer sign required") str, err := nextString("pointer sign required")
if err != nil { if err != nil {
@@ -2720,6 +2969,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.HeaderLines, err = nextInt("number of header lines required"); err != nil { if opts.HeaderLines, err = nextInt("number of header lines required"); err != nil {
return err return err
} }
case "--no-footer":
opts.Footer = []string{}
case "--footer":
str, err := nextString("footer string required")
if err != nil {
return err
}
opts.Footer = strLines(str)
case "--header-first": case "--header-first":
opts.HeaderFirst = true opts.HeaderFirst = true
case "--no-header-first": case "--no-header-first":
@@ -2764,7 +3021,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Preview.border = tui.BorderNone opts.Preview.border = tui.BorderNone
case "--preview-border": case "--preview-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.Preview.border, err = parseBorder(arg, !hasArg, true); err != nil { if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--height": case "--height":
@@ -2803,14 +3060,23 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.BorderShape = tui.BorderNone opts.BorderShape = tui.BorderNone
case "--border": case "--border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.BorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--list-border": case "--list-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.ListBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.ListBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
if opts.ListBorderShape == tui.BorderLine {
if hasArg {
// '--list-border line' is not allowed
return errors.New("list border cannot be 'line'")
}
// This is when '--style full:line' is previously specified and
// '--list-border' is specified without an argument.
opts.ListBorderShape = tui.BorderRounded
}
case "--no-list-border": case "--no-list-border":
opts.ListBorderShape = tui.BorderNone opts.ListBorderShape = tui.BorderNone
case "--no-list-label": case "--no-list-label":
@@ -2832,14 +3098,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderBorderShape = tui.BorderNone opts.HeaderBorderShape = tui.BorderNone
case "--header-border": case "--header-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-header-lines-border": case "--no-header-lines-border":
opts.HeaderLinesShape = tui.BorderNone opts.HeaderLinesShape = tui.BorderUndefined
case "--header-lines-border": case "--header-lines-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.HeaderLinesShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-header-label": case "--no-header-label":
@@ -2856,11 +3122,32 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil { if err := parseLabelPosition(&opts.HeaderLabel, pos); err != nil {
return err return err
} }
case "--no-footer-border":
opts.FooterBorderShape = tui.BorderNone
case "--footer-border":
hasArg, arg := optionalNextString()
if opts.FooterBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--no-footer-label":
opts.FooterLabel.label = ""
case "--footer-label":
if opts.FooterLabel.label, err = nextString("footer label required"); err != nil {
return err
}
case "--footer-label-pos":
pos, err := nextString("footer label position required (positive or negative integer or 'center')")
if err != nil {
return err
}
if err := parseLabelPosition(&opts.FooterLabel, pos); err != nil {
return err
}
case "--no-input-border": case "--no-input-border":
opts.InputBorderShape = tui.BorderNone opts.InputBorderShape = tui.BorderNone
case "--input-border": case "--input-border":
hasArg, arg := optionalNextString() hasArg, arg := optionalNextString()
if opts.InputBorderShape, err = parseBorder(arg, !hasArg, false); err != nil { if opts.InputBorderShape, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--no-input-label": case "--no-input-label":
@@ -3068,6 +3355,7 @@ func applyPreset(opts *Options, preset string) error {
opts.ListBorderShape = tui.BorderUndefined opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderUndefined
opts.Preview.border = defaultBorderShape opts.Preview.border = defaultBorderShape
opts.Preview.info = true opts.Preview.info = true
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
@@ -3079,6 +3367,7 @@ func applyPreset(opts *Options, preset string) error {
opts.ListBorderShape = tui.BorderUndefined opts.ListBorderShape = tui.BorderUndefined
opts.InputBorderShape = tui.BorderUndefined opts.InputBorderShape = tui.BorderUndefined
opts.HeaderBorderShape = tui.BorderUndefined opts.HeaderBorderShape = tui.BorderUndefined
opts.FooterBorderShape = tui.BorderLine
opts.Preview.border = tui.BorderLine opts.Preview.border = tui.BorderLine
opts.Preview.info = false opts.Preview.info = false
opts.InfoStyle = infoDefault opts.InfoStyle = infoDefault
@@ -3094,16 +3383,22 @@ func applyPreset(opts *Options, preset string) error {
} }
if len(tokens) == 2 && len(tokens[1]) > 0 { if len(tokens) == 2 && len(tokens[1]) > 0 {
var err error var err error
defaultBorderShape, err = parseBorder(tokens[1], false, false) defaultBorderShape, err = parseBorder(tokens[1], false)
if err != nil { if err != nil {
return err return err
} }
} }
if defaultBorderShape != tui.BorderLine {
opts.ListBorderShape = defaultBorderShape opts.ListBorderShape = defaultBorderShape
}
opts.InputBorderShape = defaultBorderShape opts.InputBorderShape = defaultBorderShape
opts.HeaderBorderShape = defaultBorderShape opts.HeaderBorderShape = defaultBorderShape
opts.FooterBorderShape = defaultBorderShape
opts.Preview.border = defaultBorderShape opts.Preview.border = defaultBorderShape
if defaultBorderShape == tui.BorderLine {
opts.BorderShape = defaultBorderShape
}
opts.Preview.info = true opts.Preview.info = true
opts.InfoStyle = infoInlineRight opts.InfoStyle = infoInlineRight
opts.Theme.Gutter = tui.NewColorAttr() opts.Theme.Gutter = tui.NewColorAttr()
@@ -3114,26 +3409,31 @@ func applyPreset(opts *Options, preset string) error {
return nil return nil
} }
func validateSign(sign string, signOptName string) error { func validateSign(sign string, signOptName string, maxWidth int) error {
if uniseg.StringWidth(sign) > 2 { if uniseg.StringWidth(sign) > maxWidth {
return fmt.Errorf("%v display width should be up to 2", signOptName) return fmt.Errorf("%v display width should be up to %d", signOptName, maxWidth)
} }
return nil return nil
} }
func validateOptions(opts *Options) error { func validateOptions(opts *Options) error {
if opts.Pointer != nil { if opts.Pointer != nil {
if err := validateSign(*opts.Pointer, "pointer"); err != nil { if err := validateSign(*opts.Pointer, "pointer", 2); err != nil {
return err return err
} }
} }
if opts.Marker != nil { if opts.Marker != nil {
if err := validateSign(*opts.Marker, "marker"); err != nil { if err := validateSign(*opts.Marker, "marker", 2); err != nil {
return err return err
} }
} }
if opts.Gutter != nil && uniseg.StringWidth(*opts.Gutter) != 1 ||
opts.GutterRaw != nil && uniseg.StringWidth(*opts.GutterRaw) != 1 {
return errors.New("gutter display width should be 1")
}
if opts.Scrollbar != nil { if opts.Scrollbar != nil {
runes := []rune(*opts.Scrollbar) runes := []rune(*opts.Scrollbar)
if len(runes) > 2 { if len(runes) > 2 {
@@ -3176,6 +3476,10 @@ func noSeparatorLine(style infoStyle, separator bool) bool {
return false return false
} }
func (opts *Options) useTmux() bool {
return opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index
}
func (opts *Options) noSeparatorLine() bool { func (opts *Options) noSeparatorLine() bool {
if opts.Inputless { if opts.Inputless {
return true return true
@@ -3207,17 +3511,12 @@ func postProcessOptions(opts *Options) error {
opts.HeaderBorderShape = tui.BorderNone opts.HeaderBorderShape = tui.BorderNone
} }
if opts.FooterBorderShape == tui.BorderUndefined {
opts.FooterBorderShape = tui.BorderLine
}
if opts.HeaderLinesShape == tui.BorderNone { if opts.HeaderLinesShape == tui.BorderNone {
opts.HeaderLinesShape = tui.BorderPhantom opts.HeaderLinesShape = tui.BorderPhantom
} else if opts.HeaderLinesShape == tui.BorderUndefined {
// In reverse-list layout, header lines should be at the top, while
// ordinary header should be at the bottom. So let's use a separate
// window for the header lines.
if opts.Layout == layoutReverseList {
opts.HeaderLinesShape = tui.BorderPhantom
} else {
opts.HeaderLinesShape = tui.BorderNone
}
} }
if opts.Pointer == nil { if opts.Pointer == nil {
@@ -3334,23 +3633,6 @@ func postProcessOptions(opts *Options) error {
} }
} }
if opts.Bold {
theme := opts.Theme
boldify := func(c tui.ColorAttr) tui.ColorAttr {
dup := c
if (c.Attr & tui.AttrRegular) == 0 {
dup.Attr |= tui.BoldForce
}
return dup
}
theme.Current = boldify(theme.Current)
theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner)
}
// If --height option is not supported on the platform, just ignore it // If --height option is not supported on the platform, just ignore it
if !tui.IsLightRendererSupported() && opts.Height.size > 0 { if !tui.IsLightRendererSupported() && opts.Height.size > 0 {
opts.Height = heightSpec{} opts.Height = heightSpec{}

View File

@@ -142,7 +142,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)
@@ -168,7 +168,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)
} }
@@ -182,7 +182,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)
} }
@@ -211,40 +211,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-,")
} }
@@ -300,8 +300,12 @@ func TestBind(t *testing.T) {
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
var base *tui.ColorTheme
theme := tui.Dark256 theme := tui.Dark256
dark, _ := parseTheme(theme, "dark") base, dark, _ := parseTheme(theme, "dark")
if *dark != *base {
t.Errorf("incorrect base theme returned")
}
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
@@ -309,7 +313,10 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent") t.Errorf("point should not be equivalent")
} }
light, _ := parseTheme(theme, "dark,light") base, light, _ := parseTheme(theme, "dark,light")
if *light != *base {
t.Errorf("incorrect base theme returned")
}
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
@@ -320,7 +327,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")
} }
@@ -333,7 +340,7 @@ 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")
} }
@@ -350,8 +357,8 @@ func TestDefaultCtrlNP(t *testing.T) {
t.Error() t.Error()
} }
} }
check([]string{}, tui.CtrlN, actDown) check([]string{}, tui.CtrlN, actDownMatch)
check([]string{}, tui.CtrlP, actUp) check([]string{}, tui.CtrlP, actUpMatch)
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
@@ -462,7 +469,7 @@ func TestValidateSign(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
err := validateSign(testCase.inputSign, "") err := validateSign(testCase.inputSign, "", 2)
if testCase.isValid && err != nil { if testCase.isValid && err != nil {
t.Errorf("Input sign `%s` caused error", testCase.inputSign) t.Errorf("Input sign `%s` caused error", testCase.inputSign)
} }

View File

@@ -90,16 +90,16 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
} }
} }
// * Write the command to a temporary file and run it with sh to ensure POSIX compliance. // Write the command to a temporary file and run it with sh to ensure POSIX compliance.
// * Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined. var exports []string
exports := []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
needBash := false needBash := false
if withExports { if withExports {
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
for _, pairStr := range os.Environ() { for _, pairStr := range os.Environ() {
pair := strings.SplitN(pairStr, "=", 2) pair := strings.SplitN(pairStr, "=", 2)
// TMUX_PANE is never set inside a tmux popup, and should not be set so as to not be detected as a regular tmux pane if validIdentifier.MatchString(pair[0]) {
if validIdentifier.MatchString(pair[0]) && pair[0] != "TMUX_PANE" {
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))) exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") { } else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
name := pair[0][10 : len(pair[0])-2] name := pair[0][10 : len(pair[0])-2]

View File

@@ -285,7 +285,7 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
if strings.HasPrefix(ignore, sep) { if strings.HasPrefix(ignore, sep) {
ignoresSuffix = append(ignoresSuffix, ignore) ignoresSuffix = append(ignoresSuffix, ignore)
} else { } else {
// 'foo/bar' should match match // 'foo/bar' should match
// * 'foo/bar' // * 'foo/bar'
// * 'baz/foo/bar' // * 'baz/foo/bar'
// * but NOT 'bazfoo/bar' // * but NOT 'bazfoo/bar'

View File

@@ -19,6 +19,10 @@ type colorOffset struct {
url *url url *url
} }
func (co colorOffset) IsFullBgMarker(at int32) bool {
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
}
type Result struct { type Result struct {
item *Item item *Item
points [4]uint16 points [4]uint16
@@ -119,14 +123,14 @@ func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
} }
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset { func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
itemColors := result.item.Colors() itemColors := result.item.Colors()
// No ANSI codes // No ANSI codes
if len(itemColors) == 0 && len(nthOffsets) == 0 { if len(itemColors) == 0 && len(nthOffsets) == 0 {
var offsets []colorOffset offsets := make([]colorOffset, len(matchOffsets))
for _, off := range matchOffsets { for i, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}) offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
} }
return offsets return offsets
} }
@@ -149,12 +153,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color bool color bool
match bool match bool
nth bool nth bool
fbg tui.Color
} }
cols := make([]cellInfo, maxCol) cols := make([]cellInfo, maxCol+1)
for idx := range cols {
cols[idx].fbg = -1
}
for colorIndex, ansi := range itemColors { for colorIndex, ansi := range itemColors {
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
cols[ansi.offset[0]].fbg = ansi.color.lbg
} else {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ { for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = cellInfo{colorIndex, true, false, false} cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
}
} }
} }
@@ -176,29 +188,35 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
// ------------ ---- -- ---- // ------------ ---- -- ----
// ++++++++ ++++++++++ // ++++++++ ++++++++++
// --++++++++-- --++++++++++--- // --++++++++-- --++++++++++---
var curr cellInfo = cellInfo{0, false, false, false} curr := cellInfo{0, false, false, false, -1}
start := 0 start := 0
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair { ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
if !theme.Colored {
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
}
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
if base.ShouldStripColors() {
return base
}
fg := ansi.color.fg fg := ansi.color.fg
bg := ansi.color.bg bg := ansi.color.bg
if fg == -1 { if fg == -1 {
if current { fg = colBase.Fg()
fg = theme.Current.Color
} else {
fg = theme.Fg.Color
}
} }
if bg == -1 { if bg == -1 {
if current { bg = colBase.Bg()
bg = theme.DarkBg.Color
} else {
bg = theme.Bg.Color
}
} }
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base) return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
} }
var colors []colorOffset var colors []colorOffset
add := func(idx int) { add := func(idx int) {
if curr.fbg >= 0 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(start)},
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
match: false,
url: nil})
}
if (curr.color || curr.nth || curr.match) && idx > start { if (curr.color || curr.nth || curr.match) && idx > start {
if curr.match { if curr.match {
var color tui.ColorPair var color tui.ColorPair
@@ -208,7 +226,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
color = colBase.Merge(colMatch) color = colBase.Merge(colMatch)
} }
var url *url var url *url
if curr.color && theme.Colored { if curr.color {
ansi := itemColors[curr.index] ansi := itemColors[curr.index]
url = ansi.color.url url = ansi.color.url
origColor := ansiToColorPair(ansi, colMatch) origColor := ansiToColorPair(ansi, colMatch)
@@ -223,7 +241,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
if color.Fg().IsDefault() && origColor.HasBg() { if color.Fg().IsDefault() && origColor.HasBg() {
color = origColor color = origColor
if curr.nth { if curr.nth {
color = color.WithAttr(attrNth) color = color.WithAttr(attrNth &^ tui.AttrRegular)
} }
} else { } else {
color = origColor.MergeNonDefault(color) color = origColor.MergeNonDefault(color)
@@ -233,19 +251,27 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url}) offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
} else if curr.color { } else if curr.color {
ansi := itemColors[curr.index] ansi := itemColors[curr.index]
color := ansiToColorPair(ansi, colBase) base := colBase
if curr.nth { if curr.nth {
color = color.WithAttr(attrNth) base = base.WithAttr(attrNth)
} }
if hidden {
base = base.WithFg(theme.Nomatch)
}
color := ansiToColorPair(ansi, base)
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: color, color: color,
match: false, match: false,
url: ansi.color.url}) url: ansi.color.url})
} else { } else {
color := colBase.WithAttr(attrNth)
if hidden {
color = color.WithFg(theme.Nomatch)
}
colors = append(colors, colorOffset{ colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: colBase.WithAttr(attrNth), color: color,
match: false, match: false,
url: nil}) url: nil})
} }

View File

@@ -131,7 +131,7 @@ func TestColorOffset(t *testing.T) {
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined) colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined) colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true) colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
assert := func(idx int, b int32, e int32, c tui.ColorPair) { assert := func(idx int, b int32, e int32, c tui.ColorPair) {
o := colors[idx] o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c { if o.offset[0] != b || o.offset[1] != e || o.color != c {
@@ -158,7 +158,7 @@ func TestColorOffset(t *testing.T) {
nthOffsets := []Offset{{37, 39}, {42, 45}} nthOffsets := []Offset{{37, 39}, {42, 45}}
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} { for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true) colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}} // [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
@@ -176,7 +176,7 @@ func TestColorOffset(t *testing.T) {
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold)) assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
expected := tui.Bold | attr expected := tui.Bold | attr
if attr == tui.AttrRegular { if attr == tui.AttrRegular {
expected = tui.AttrRegular expected = tui.Bold
} }
assert(10, 37, 39, tui.NewColorPair(4, 8, expected)) assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold)) assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))

View File

@@ -46,15 +46,20 @@ type httpServer struct {
type listenAddress struct { type listenAddress struct {
host string host string
port int port int
sock string
} }
func (addr listenAddress) IsLocal() bool { func (addr listenAddress) IsLocal() bool {
return addr.host == "localhost" || addr.host == "127.0.0.1" return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
} }
var defaultListenAddr = listenAddress{"localhost", 0} var defaultListenAddr = listenAddress{"localhost", 0, ""}
func parseListenAddress(address string) (listenAddress, error) { func parseListenAddress(address string) (listenAddress, error) {
if strings.HasSuffix(address, ".sock") {
return listenAddress{"", 0, address}, nil
}
parts := strings.SplitN(address, ":", 3) parts := strings.SplitN(address, ":", 3)
if len(parts) == 1 { if len(parts) == 1 {
parts = []string{"localhost", parts[0]} parts = []string{"localhost", parts[0]}
@@ -70,7 +75,7 @@ func parseListenAddress(address string) (listenAddress, error) {
if len(parts[0]) == 0 { if len(parts[0]) == 0 {
parts[0] = "localhost" parts[0] = "localhost"
} }
return listenAddress{parts[0], port}, nil return listenAddress{parts[0], port, ""}, nil
} }
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) { func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
@@ -80,8 +85,26 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return nil, port, errors.New("FZF_API_KEY is required to allow remote access") return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
} }
var listener net.Listener
var err error
if len(address.sock) > 0 {
if _, err := os.Stat(address.sock); err == nil {
// Check if the socket is already in use
if conn, err := net.Dial("unix", address.sock); err == nil {
conn.Close()
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
}
os.Remove(address.sock)
}
listener, err = net.Listen("unix", address.sock)
if err != nil {
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
}
os.Chmod(address.sock, 0600)
} else {
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 nil, port, fmt.Errorf("failed to listen on %s", addrStr) return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
} }
@@ -97,6 +120,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
return nil, port, err return nil, port, err
} }
} }
}
server := httpServer{ server := httpServer{
apiKey: []byte(apiKey), apiKey: []byte(apiKey),

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string { func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
replaced, _ := replacePlaceholder(replacePlaceholderParams{ replaced, _ := replacePlaceholder(replacePlaceholderParams{
template: template, template: template,
stripAnsi: stripAnsi, stripAnsi: stripAnsi,
@@ -30,11 +30,11 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1} items1 := [3][]*Item{{item1}, {item1}, nil}
items2 := []*Item{ items2 := [3][]*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"), {newItem("foo'bar \x1b[31mbaz\x1b[m")},
newItem("foo'bar \x1b[31mbaz\x1b[m"), {newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
delim := "'" delim := "'"
var regex *regexp.Regexp var regex *regexp.Regexp
@@ -145,11 +145,11 @@ func TestReplacePlaceholder(t *testing.T) {
checkFormat("echo {{.O}} {{.O}}") checkFormat("echo {{.O}} {{.O}}")
// No match // No match
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
check("echo /") check("echo /")
// No match, but with selections // No match, but with selections
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1}) result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}") checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
// String delimiter // String delimiter
@@ -166,17 +166,18 @@ func TestReplacePlaceholder(t *testing.T) {
Test single placeholders, but focus on the placeholders' parameters (e.g. flags). Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
see: TestParsePlaceholder see: TestParsePlaceholder
*/ */
items3 := []*Item{ items3 := [3][]*Item{
// single line // single line
newItem("1a 1b 1c 1d 1e 1f"), {newItem("1a 1b 1c 1d 1e 1f")},
// multi line // multi line
newItem("1a 1b 1c 1d 1e 1f"), {newItem("1a 1b 1c 1d 1e 1f"),
newItem("2a 2b 2c 2d 2e 2f"), newItem("2a 2b 2c 2d 2e 2f"),
newItem("3a 3b 3c 3d 3e 3f"), newItem("3a 3b 3c 3d 3e 3f"),
newItem("4a 4b 4c 4d 4e 4f"), newItem("4a 4b 4c 4d 4e 4f"),
newItem("5a 5b 5c 5d 5e 5f"), newItem("5a 5b 5c 5d 5e 5f"),
newItem("6a 6b 6c 6d 6e 6f"), newItem("6a 6b 6c 6d 6e 6f"),
newItem("7a 7b 7c 7d 7e 7f"), newItem("7a 7b 7c 7d 7e 7f")},
nil,
} }
stripAnsi := false stripAnsi := false
forcePlus := false forcePlus := false
@@ -557,14 +558,14 @@ func newItem(str string) *Item {
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
} }
// Functions tested in this file require array of items (allItems). The array needs // Functions tested in this file require array of items (allItems).
// to consist of at least two nils. This is helper function. // This is helper function.
func newItems(str ...string) []*Item { func newItems(str ...string) [3][]*Item {
result := make([]*Item, util.Max(len(str), 2)) result := make([]*Item, len(str))
for i, s := range str { for i, s := range str {
result[i] = newItem(s) result[i] = newItem(s)
} }
return result return [3][]*Item{result, nil, nil}
} }
// (for logging purposes) // (for logging purposes)
@@ -588,7 +589,7 @@ func templateToString(format string, data any) string {
type give struct { type give struct {
template string template string
query string query string
allItems []*Item allItems [3][]*Item
} }
type want struct { type want struct {
/* /*
@@ -626,25 +627,25 @@ func testCommands(t *testing.T, tests []testCase) {
// evaluate the test cases // evaluate the test cases
for idx, test := range tests { for idx, test := range tests {
gotOutput := replacePlaceholderTest( gotOutput := replacePlaceholderTest(
test.give.template, stripAnsi, delimiter, printsep, forcePlus, test.template, stripAnsi, delimiter, printsep, forcePlus,
test.give.query, test.query,
test.give.allItems) test.allItems)
switch { switch {
case test.want.output != "": case test.output != "":
if gotOutput != test.want.output { if gotOutput != test.output {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx, idx,
test.give.template, test.give.query, test.give.allItems, test.template, test.query, test.allItems,
gotOutput, test.want.output) gotOutput, test.output)
} }
case test.want.match != "": case test.match != "":
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`) wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
wantRegex := regexp.MustCompile(wantMatch) wantRegex := regexp.MustCompile(wantMatch)
if !wantRegex.MatchString(gotOutput) { if !wantRegex.MatchString(gotOutput) {
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'", t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
idx, idx,
test.give.template, test.give.query, test.give.allItems, test.template, test.query, test.allItems,
gotOutput, test.want.match) gotOutput, test.match)
} }
default: default:
t.Errorf("tests[%v]: test case does not describe 'want' property", idx) t.Errorf("tests[%v]: test case does not describe 'want' property", idx)

View File

@@ -11,10 +11,14 @@ func runTmux(args []string, opts *Options) (int, error) {
// Prepare arguments // Prepare arguments
fzf, rest := args[0], args[1:] fzf, rest := args[0], args[1:]
args = []string{"--bind=ctrl-z:ignore"} args = []string{"--bind=ctrl-z:ignore"}
if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined { if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
// We append --border option at the end, because `--style=full:STYLE` // We append --border option at the end, because `--style=full:STYLE`
// may have changed the default border style. // may have changed the default border style.
rest = append(rest, "--border") if tui.DefaultBorderShape == tui.BorderRounded {
rest = append(rest, "--border=rounded")
} else {
rest = append(rest, "--border=sharp")
}
} }
if opts.Tmux.border && opts.Margin == defaultMargin() { if opts.Tmux.border && opts.Margin == defaultMargin() {
args = append(args, "--margin=0,1") args = append(args, "--margin=0,1")

View File

@@ -2,29 +2,7 @@
package tui package tui
type Attr int32
func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
return a | b
}
const ( const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
Bold = Attr(1) Bold = Attr(1)
Dim = Attr(1 << 1) Dim = Attr(1 << 1)
Italic = Attr(1 << 2) Italic = Attr(1 << 2)
@@ -35,6 +13,12 @@ const (
StrikeThrough = Attr(1 << 7) StrikeThrough = Attr(1 << 7)
) )
func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape = BorderRounded
func (r *FullscreenRenderer) Init() error { return nil } func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil } func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}

View File

@@ -37,84 +37,137 @@ func _() {
_ = x[CtrlZ-26] _ = x[CtrlZ-26]
_ = x[Esc-27] _ = x[Esc-27]
_ = x[CtrlSpace-28] _ = x[CtrlSpace-28]
_ = x[CtrlDelete-29] _ = x[CtrlBackSlash-29]
_ = x[CtrlBackSlash-30] _ = x[CtrlRightBracket-30]
_ = x[CtrlRightBracket-31] _ = x[CtrlCaret-31]
_ = x[CtrlCaret-32] _ = x[CtrlSlash-32]
_ = x[CtrlSlash-33] _ = x[ShiftTab-33]
_ = x[ShiftTab-34] _ = x[Backspace-34]
_ = x[Backspace-35] _ = x[Delete-35]
_ = x[Delete-36] _ = x[PageUp-36]
_ = x[PageUp-37] _ = x[PageDown-37]
_ = x[PageDown-38] _ = x[Up-38]
_ = x[Up-39] _ = x[Down-39]
_ = x[Down-40] _ = x[Left-40]
_ = x[Left-41] _ = x[Right-41]
_ = x[Right-42] _ = x[Home-42]
_ = x[Home-43] _ = x[End-43]
_ = x[End-44] _ = x[Insert-44]
_ = x[Insert-45] _ = x[ShiftUp-45]
_ = x[ShiftUp-46] _ = x[ShiftDown-46]
_ = x[ShiftDown-47] _ = x[ShiftLeft-47]
_ = x[ShiftLeft-48] _ = x[ShiftRight-48]
_ = x[ShiftRight-49] _ = x[ShiftDelete-49]
_ = x[ShiftDelete-50] _ = x[ShiftHome-50]
_ = x[F1-51] _ = x[ShiftEnd-51]
_ = x[F2-52] _ = x[ShiftPageUp-52]
_ = x[F3-53] _ = x[ShiftPageDown-53]
_ = x[F4-54] _ = x[F1-54]
_ = x[F5-55] _ = x[F2-55]
_ = x[F6-56] _ = x[F3-56]
_ = x[F7-57] _ = x[F4-57]
_ = x[F8-58] _ = x[F5-58]
_ = x[F9-59] _ = x[F6-59]
_ = x[F10-60] _ = x[F7-60]
_ = x[F11-61] _ = x[F8-61]
_ = x[F12-62] _ = x[F9-62]
_ = x[AltBackspace-63] _ = x[F10-63]
_ = x[AltUp-64] _ = x[F11-64]
_ = x[AltDown-65] _ = x[F12-65]
_ = x[AltLeft-66] _ = x[AltBackspace-66]
_ = x[AltRight-67] _ = x[AltUp-67]
_ = x[AltShiftUp-68] _ = x[AltDown-68]
_ = x[AltShiftDown-69] _ = x[AltLeft-69]
_ = x[AltShiftLeft-70] _ = x[AltRight-70]
_ = x[AltShiftRight-71] _ = x[AltDelete-71]
_ = x[Alt-72] _ = x[AltHome-72]
_ = x[CtrlAlt-73] _ = x[AltEnd-73]
_ = x[Invalid-74] _ = x[AltPageUp-74]
_ = x[Fatal-75] _ = x[AltPageDown-75]
_ = x[BracketedPasteBegin-76] _ = x[AltShiftUp-76]
_ = x[BracketedPasteEnd-77] _ = x[AltShiftDown-77]
_ = x[Mouse-78] _ = x[AltShiftLeft-78]
_ = x[DoubleClick-79] _ = x[AltShiftRight-79]
_ = x[LeftClick-80] _ = x[AltShiftDelete-80]
_ = x[RightClick-81] _ = x[AltShiftHome-81]
_ = x[SLeftClick-82] _ = x[AltShiftEnd-82]
_ = x[SRightClick-83] _ = x[AltShiftPageUp-83]
_ = x[ScrollUp-84] _ = x[AltShiftPageDown-84]
_ = x[ScrollDown-85] _ = x[CtrlUp-85]
_ = x[SScrollUp-86] _ = x[CtrlDown-86]
_ = x[SScrollDown-87] _ = x[CtrlLeft-87]
_ = x[PreviewScrollUp-88] _ = x[CtrlRight-88]
_ = x[PreviewScrollDown-89] _ = x[CtrlHome-89]
_ = x[Resize-90] _ = x[CtrlEnd-90]
_ = x[Change-91] _ = x[CtrlBackspace-91]
_ = x[BackwardEOF-92] _ = x[CtrlDelete-92]
_ = x[Start-93] _ = x[CtrlPageUp-93]
_ = x[Load-94] _ = x[CtrlPageDown-94]
_ = x[Focus-95] _ = x[Alt-95]
_ = x[One-96] _ = x[CtrlAlt-96]
_ = x[Zero-97] _ = x[CtrlAltUp-97]
_ = x[Result-98] _ = x[CtrlAltDown-98]
_ = x[Jump-99] _ = x[CtrlAltLeft-99]
_ = x[JumpCancel-100] _ = x[CtrlAltRight-100]
_ = x[ClickHeader-101] _ = x[CtrlAltHome-101]
_ = x[CtrlAltEnd-102]
_ = x[CtrlAltBackspace-103]
_ = x[CtrlAltDelete-104]
_ = x[CtrlAltPageUp-105]
_ = x[CtrlAltPageDown-106]
_ = x[CtrlShiftUp-107]
_ = x[CtrlShiftDown-108]
_ = x[CtrlShiftLeft-109]
_ = x[CtrlShiftRight-110]
_ = x[CtrlShiftHome-111]
_ = x[CtrlShiftEnd-112]
_ = x[CtrlShiftDelete-113]
_ = x[CtrlShiftPageUp-114]
_ = x[CtrlShiftPageDown-115]
_ = x[CtrlAltShiftUp-116]
_ = x[CtrlAltShiftDown-117]
_ = x[CtrlAltShiftLeft-118]
_ = x[CtrlAltShiftRight-119]
_ = x[CtrlAltShiftHome-120]
_ = x[CtrlAltShiftEnd-121]
_ = x[CtrlAltShiftDelete-122]
_ = x[CtrlAltShiftPageUp-123]
_ = x[CtrlAltShiftPageDown-124]
_ = x[Invalid-125]
_ = x[Fatal-126]
_ = x[BracketedPasteBegin-127]
_ = x[BracketedPasteEnd-128]
_ = x[Mouse-129]
_ = x[DoubleClick-130]
_ = x[LeftClick-131]
_ = x[RightClick-132]
_ = x[SLeftClick-133]
_ = x[SRightClick-134]
_ = x[ScrollUp-135]
_ = x[ScrollDown-136]
_ = x[SScrollUp-137]
_ = x[SScrollDown-138]
_ = x[PreviewScrollUp-139]
_ = x[PreviewScrollDown-140]
_ = x[Resize-141]
_ = x[Change-142]
_ = x[BackwardEOF-143]
_ = x[Start-144]
_ = x[Load-145]
_ = x[Focus-146]
_ = x[One-147]
_ = x[Zero-148]
_ = x[Result-149]
_ = x[Jump-150]
_ = x[JumpCancel-151]
_ = x[ClickHeader-152]
_ = x[ClickFooter-153]
_ = x[Multi-154]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
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, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684} 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, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
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) {

View File

@@ -335,6 +335,8 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{Backspace, 0, nil} return Event{Backspace, 0, nil}
case 8:
return Event{CtrlBackspace, 0, nil}
case 0: case 0:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case 28: case 28:
@@ -381,6 +383,9 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
*sz = 2 *sz = 2
if r.buffer[1] == 8 {
return Event{CtrlAltBackspace, 0, nil}
}
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
} }
@@ -473,22 +478,139 @@ func (r *LightRenderer) escSequence(sz *int) Event {
if r.buffer[3] == '~' { if r.buffer[3] == '~' {
return Event{Delete, 0, nil} return Event{Delete, 0, nil}
} }
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftDelete, 0, nil}
case '1':
return Event{AltDelete, 0, nil}
case '2':
return Event{AltShiftDelete, 0, nil}
case '3':
return Event{CtrlAltDelete, 0, nil}
case '4':
return Event{CtrlAltShiftDelete, 0, nil}
case '5':
return Event{CtrlAltDelete, 0, nil}
case '6':
return Event{CtrlAltShiftDelete, 0, nil}
}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' { if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case '5':
return Event{CtrlDelete, 0, nil}
case '2': case '2':
return Event{ShiftDelete, 0, nil} return Event{ShiftDelete, 0, nil}
case '3':
return Event{AltDelete, 0, nil}
case '4':
return Event{AltShiftDelete, 0, nil}
case '5':
return Event{CtrlDelete, 0, nil}
case '6':
return Event{CtrlShiftDelete, 0, nil}
case '7':
return Event{CtrlAltDelete, 0, nil}
case '8':
return Event{CtrlAltShiftDelete, 0, nil}
case '9':
return Event{AltDelete, 0, nil}
} }
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
case '4': case '4':
return Event{End, 0, nil} return Event{End, 0, nil}
case '5': case '5':
if r.buffer[3] == '~' {
return Event{PageUp, 0, nil} return Event{PageUp, 0, nil}
}
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftPageUp, 0, nil}
case '1':
return Event{AltPageUp, 0, nil}
case '2':
return Event{AltShiftPageUp, 0, nil}
case '3':
return Event{CtrlAltPageUp, 0, nil}
case '4':
return Event{CtrlAltShiftPageUp, 0, nil}
case '5':
return Event{CtrlAltPageUp, 0, nil}
case '6': case '6':
return Event{CtrlAltShiftPageUp, 0, nil}
}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '2':
return Event{ShiftPageUp, 0, nil}
case '3':
return Event{AltPageUp, 0, nil}
case '4':
return Event{AltShiftPageUp, 0, nil}
case '5':
return Event{CtrlPageUp, 0, nil}
case '6':
return Event{CtrlShiftPageUp, 0, nil}
case '7':
return Event{CtrlAltPageUp, 0, nil}
case '8':
return Event{CtrlAltShiftPageUp, 0, nil}
case '9':
return Event{AltPageUp, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '6':
if r.buffer[3] == '~' {
return Event{PageDown, 0, nil} return Event{PageDown, 0, nil}
}
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
*sz = 7
switch r.buffer[5] {
case '0':
return Event{AltShiftPageDown, 0, nil}
case '1':
return Event{AltPageDown, 0, nil}
case '2':
return Event{AltShiftPageDown, 0, nil}
case '3':
return Event{CtrlAltPageDown, 0, nil}
case '4':
return Event{CtrlAltShiftPageDown, 0, nil}
case '5':
return Event{CtrlAltPageDown, 0, nil}
case '6':
return Event{CtrlAltShiftPageDown, 0, nil}
}
}
if len(r.buffer) == 6 && r.buffer[5] == '~' {
*sz = 6
switch r.buffer[4] {
case '2':
return Event{ShiftPageDown, 0, nil}
case '3':
return Event{AltPageDown, 0, nil}
case '4':
return Event{AltShiftPageDown, 0, nil}
case '5':
return Event{CtrlPageDown, 0, nil}
case '6':
return Event{CtrlShiftPageDown, 0, nil}
case '7':
return Event{CtrlAltPageDown, 0, nil}
case '8':
return Event{CtrlAltShiftPageDown, 0, nil}
case '9':
return Event{AltPageDown, 0, nil}
}
}
return Event{Invalid, 0, nil}
case '7': case '7':
return Event{Home, 0, nil} return Event{Home, 0, nil}
case '8': case '8':
@@ -526,64 +648,173 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[4] { switch r.buffer[4] {
case '1', '2', '3', '4', '5': case '1', '2', '3', '4', '5', '6', '7', '8', '9':
// Kitty iTerm2 WezTerm // Kitty iTerm2 WezTerm
// SHIFT-ARROW "\e[1;2D" // SHIFT-ARROW "\e[1;2D"
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D" // ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
// CTRL-SHIFT-ARROW "\e[1;6D" N/A // CTRL-SHIFT-ARROW "\e[1;6D" N/A
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D") // CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
alt := r.buffer[4] == '3' ctrl := bytes.IndexByte([]byte{'5', '6', '7', '8'}, r.buffer[4]) >= 0
alt := bytes.IndexByte([]byte{'3', '4', '7', '8'}, r.buffer[4]) >= 0
shift := bytes.IndexByte([]byte{'2', '4', '6', '8'}, r.buffer[4]) >= 0
char := r.buffer[5] char := r.buffer[5]
altShift := false if r.buffer[4] == '9' {
if r.buffer[4] == '1' && r.buffer[5] == '0' { ctrl = false
altShift = true alt = true
if len(r.buffer) < 7 { shift = false
return Event{Invalid, 0, nil}
}
*sz = 7
char = r.buffer[6]
} else if r.buffer[4] == '4' {
altShift = true
if len(r.buffer) < 6 { if len(r.buffer) < 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
*sz = 6 *sz = 6
char = r.buffer[5] char = r.buffer[5]
} else if r.buffer[4] == '1' && bytes.IndexByte([]byte{'0', '1', '2', '3', '4', '5', '6'}, r.buffer[5]) >= 0 {
ctrl = bytes.IndexByte([]byte{'3', '4', '5', '6'}, r.buffer[5]) >= 0
alt = true
shift = bytes.IndexByte([]byte{'0', '2', '4', '6'}, r.buffer[5]) >= 0
if len(r.buffer) < 7 {
return Event{Invalid, 0, nil}
} }
*sz = 7
char = r.buffer[6]
}
ctrlShift := ctrl && shift
ctrlAlt := ctrl && alt
altShift := alt && shift
ctrlAltShift := ctrl && alt && shift
switch char { switch char {
case 'A': case 'A':
if alt { if ctrlAltShift {
return Event{AltUp, 0, nil} return Event{CtrlAltShiftUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftUp, 0, nil}
} }
if altShift { if altShift {
return Event{AltShiftUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
return Event{ShiftUp, 0, nil} if ctrl {
case 'B': return Event{CtrlUp, 0, nil}
}
if alt { if alt {
return Event{AltDown, 0, nil} return Event{AltUp, 0, nil}
}
if shift {
return Event{ShiftUp, 0, nil}
}
case 'B':
if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDown, 0, nil}
} }
if altShift { if altShift {
return Event{AltShiftDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
return Event{ShiftDown, 0, nil} if ctrl {
case 'C': return Event{CtrlDown, 0, nil}
}
if alt { if alt {
return Event{AltRight, 0, nil} return Event{AltDown, 0, nil}
}
if shift {
return Event{ShiftDown, 0, nil}
}
case 'C':
if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltRight, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftRight, 0, nil}
} }
if altShift { if altShift {
return Event{AltShiftRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
if ctrl {
return Event{CtrlRight, 0, nil}
}
if shift {
return Event{ShiftRight, 0, nil} return Event{ShiftRight, 0, nil}
case 'D': }
if alt { if alt {
return Event{AltLeft, 0, nil} return Event{AltRight, 0, nil}
}
case 'D':
if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltLeft, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftLeft, 0, nil}
} }
if altShift { if altShift {
return Event{AltShiftLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
if ctrl {
return Event{CtrlLeft, 0, nil}
}
if alt {
return Event{AltLeft, 0, nil}
}
if shift {
return Event{ShiftLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
case 'H':
if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltHome, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftHome, 0, nil}
}
if altShift {
return Event{AltShiftHome, 0, nil}
}
if ctrl {
return Event{CtrlHome, 0, nil}
}
if alt {
return Event{AltHome, 0, nil}
}
if shift {
return Event{ShiftHome, 0, nil}
}
case 'F':
if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltEnd, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftEnd, 0, nil}
}
if altShift {
return Event{AltShiftEnd, 0, nil}
}
if ctrl {
return Event{CtrlEnd, 0, nil}
}
if alt {
return Event{AltEnd, 0, nil}
}
if shift {
return Event{ShiftEnd, 0, nil}
}
}
} // r.buffer[4] } // r.buffer[4]
} // r.buffer[3] } // r.buffer[3]
} // r.buffer[2] } // r.buffer[2]
@@ -829,11 +1060,14 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind
case WindowHeader: case WindowHeader:
w.fg = r.theme.Header.Color w.fg = r.theme.Header.Color
w.bg = r.theme.HeaderBg.Color w.bg = r.theme.HeaderBg.Color
case WindowFooter:
w.fg = r.theme.Footer.Color
w.bg = r.theme.FooterBg.Color
case WindowPreview: case WindowPreview:
w.fg = r.theme.PreviewFg.Color w.fg = r.theme.PreviewFg.Color
w.bg = r.theme.PreviewBg.Color w.bg = r.theme.PreviewBg.Color
} }
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone { if erase && !w.bg.IsDefault() && w.border.shape != BorderNone && w.height > 0 {
// fzf --color bg:blue --border --padding 1,2 // fzf --color bg:blue --border --padding 1,2
w.Erase() w.Erase()
} }
@@ -889,6 +1123,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }
@@ -914,6 +1150,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }
@@ -941,6 +1179,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
color = ColInputBorder color = ColInputBorder
case WindowHeader: case WindowHeader:
color = ColHeaderBorder color = ColHeaderBorder
case WindowFooter:
color = ColFooterBorder
case WindowPreview: case WindowPreview:
color = ColPreviewBorder color = ColPreviewBorder
} }

335
src/tui/light_test.go Normal file
View File

@@ -0,0 +1,335 @@
package tui
import (
"fmt"
"os"
"testing"
"unicode"
)
func TestLightRenderer(t *testing.T) {
tty_file, _ := os.Open("")
renderer, _ := NewLightRenderer(
"", tty_file, &ColorTheme{}, true, false, 0, false, true,
func(h int) int { return h })
light_renderer := renderer.(*LightRenderer)
assertCharSequence := func(sequence string, name string) {
bytes := []byte(sequence)
light_renderer.buffer = bytes
event := light_renderer.GetChar()
if event.KeyName() != name {
t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s",
string(bytes), bytes,
event.KeyName(), event.Type.String(), name)
}
}
assertEscSequence := func(sequence string, name string) {
bytes := []byte(sequence)
light_renderer.buffer = bytes
sz := 1
event := light_renderer.escSequence(&sz)
if fmt.Sprintf("!%s", event.Type.String()) == name {
// this is fine
} else if event.KeyName() != name {
t.Errorf(
"sequence: %q | %v | '%s' (%s) != %s",
string(bytes), bytes,
event.KeyName(), event.Type.String(), name)
}
}
// invalid
assertEscSequence("\x1b[<", "!Invalid")
assertEscSequence("\x1b[1;1R", "!Invalid")
assertEscSequence("\x1b[", "!Invalid")
assertEscSequence("\x1b[1", "!Invalid")
assertEscSequence("\x1b[3;3~1", "!Invalid")
assertEscSequence("\x1b[13", "!Invalid")
assertEscSequence("\x1b[1;3", "!Invalid")
assertEscSequence("\x1b[1;10", "!Invalid")
assertEscSequence("\x1b[220~", "!Invalid")
assertEscSequence("\x1b[5;30~", "!Invalid")
assertEscSequence("\x1b[6;30~", "!Invalid")
// general
for r := 'a'; r < 'z'; r++ {
lower_r := fmt.Sprintf("%c", r)
upper_r := fmt.Sprintf("%c", unicode.ToUpper(r))
assertCharSequence(lower_r, lower_r)
assertCharSequence(upper_r, upper_r)
}
assertCharSequence("\x01", "ctrl-a")
assertCharSequence("\x02", "ctrl-b")
assertCharSequence("\x03", "ctrl-c")
assertCharSequence("\x04", "ctrl-d")
assertCharSequence("\x05", "ctrl-e")
assertCharSequence("\x06", "ctrl-f")
assertCharSequence("\x07", "ctrl-g")
// ctrl-h is the same as ctrl-backspace
// ctrl-i is the same as tab
assertCharSequence("\n", "ctrl-j")
assertCharSequence("\x0b", "ctrl-k")
assertCharSequence("\x0c", "ctrl-l")
assertCharSequence("\r", "enter") // enter
assertCharSequence("\x0e", "ctrl-n")
assertCharSequence("\x0f", "ctrl-o")
assertCharSequence("\x10", "ctrl-p")
assertCharSequence("\x11", "ctrl-q")
assertCharSequence("\x12", "ctrl-r")
assertCharSequence("\x13", "ctrl-s")
assertCharSequence("\x14", "ctrl-t")
assertCharSequence("\x15", "ctrl-u")
assertCharSequence("\x16", "ctrl-v")
assertCharSequence("\x17", "ctrl-w")
assertCharSequence("\x18", "ctrl-x")
assertCharSequence("\x19", "ctrl-y")
assertCharSequence("\x1a", "ctrl-z")
assertCharSequence("\x00", "ctrl-space")
assertCharSequence("\x1c", "ctrl-\\")
assertCharSequence("\x1d", "ctrl-]")
assertCharSequence("\x1e", "ctrl-^")
assertCharSequence("\x1f", "ctrl-/")
assertEscSequence("\x1ba", "alt-a")
assertEscSequence("\x1bb", "alt-b")
assertEscSequence("\x1bc", "alt-c")
assertEscSequence("\x1bd", "alt-d")
assertEscSequence("\x1be", "alt-e")
assertEscSequence("\x1bf", "alt-f")
assertEscSequence("\x1bg", "alt-g")
assertEscSequence("\x1bh", "alt-h")
assertEscSequence("\x1bi", "alt-i")
assertEscSequence("\x1bj", "alt-j")
assertEscSequence("\x1bk", "alt-k")
assertEscSequence("\x1bl", "alt-l")
assertEscSequence("\x1bm", "alt-m")
assertEscSequence("\x1bn", "alt-n")
assertEscSequence("\x1bo", "alt-o")
assertEscSequence("\x1bp", "alt-p")
assertEscSequence("\x1bq", "alt-q")
assertEscSequence("\x1br", "alt-r")
assertEscSequence("\x1bs", "alt-s")
assertEscSequence("\x1bt", "alt-t")
assertEscSequence("\x1bu", "alt-u")
assertEscSequence("\x1bv", "alt-v")
assertEscSequence("\x1bw", "alt-w")
assertEscSequence("\x1bx", "alt-x")
assertEscSequence("\x1by", "alt-y")
assertEscSequence("\x1bz", "alt-z")
assertEscSequence("\x1bOP", "f1")
assertEscSequence("\x1bOQ", "f2")
assertEscSequence("\x1bOR", "f3")
assertEscSequence("\x1bOS", "f4")
assertEscSequence("\x1b[15~", "f5")
assertEscSequence("\x1b[17~", "f6")
assertEscSequence("\x1b[18~", "f7")
assertEscSequence("\x1b[19~", "f8")
assertEscSequence("\x1b[20~", "f9")
assertEscSequence("\x1b[21~", "f10")
assertEscSequence("\x1b[23~", "f11")
assertEscSequence("\x1b[24~", "f12")
assertEscSequence("\x1b", "esc")
assertCharSequence("\t", "tab")
assertEscSequence("\x1b[Z", "shift-tab")
assertCharSequence("\x7f", "backspace")
assertEscSequence("\x1b\x7f", "alt-backspace")
assertCharSequence("\b", "ctrl-backspace")
assertEscSequence("\x1b\b", "ctrl-alt-backspace")
assertEscSequence("\x1b[A", "up")
assertEscSequence("\x1b[B", "down")
assertEscSequence("\x1b[C", "right")
assertEscSequence("\x1b[D", "left")
assertEscSequence("\x1b[H", "home")
assertEscSequence("\x1b[F", "end")
assertEscSequence("\x1b[2~", "insert")
assertEscSequence("\x1b[3~", "delete")
assertEscSequence("\x1b[5~", "page-up")
assertEscSequence("\x1b[6~", "page-down")
assertEscSequence("\x1b[7~", "home")
assertEscSequence("\x1b[8~", "end")
assertEscSequence("\x1b[1;2A", "shift-up")
assertEscSequence("\x1b[1;2B", "shift-down")
assertEscSequence("\x1b[1;2C", "shift-right")
assertEscSequence("\x1b[1;2D", "shift-left")
assertEscSequence("\x1b[1;2H", "shift-home")
assertEscSequence("\x1b[1;2F", "shift-end")
assertEscSequence("\x1b[3;2~", "shift-delete")
assertEscSequence("\x1b[5;2~", "shift-page-up")
assertEscSequence("\x1b[6;2~", "shift-page-down")
assertEscSequence("\x1b\x1b", "esc")
assertEscSequence("\x1b\x1b[A", "alt-up")
assertEscSequence("\x1b\x1b[B", "alt-down")
assertEscSequence("\x1b\x1b[C", "alt-right")
assertEscSequence("\x1b\x1b[D", "alt-left")
assertEscSequence("\x1b[1;3A", "alt-up")
assertEscSequence("\x1b[1;3B", "alt-down")
assertEscSequence("\x1b[1;3C", "alt-right")
assertEscSequence("\x1b[1;3D", "alt-left")
assertEscSequence("\x1b[1;3H", "alt-home")
assertEscSequence("\x1b[1;3F", "alt-end")
assertEscSequence("\x1b[3;3~", "alt-delete")
assertEscSequence("\x1b[5;3~", "alt-page-up")
assertEscSequence("\x1b[6;3~", "alt-page-down")
assertEscSequence("\x1b[1;4A", "alt-shift-up")
assertEscSequence("\x1b[1;4B", "alt-shift-down")
assertEscSequence("\x1b[1;4C", "alt-shift-right")
assertEscSequence("\x1b[1;4D", "alt-shift-left")
assertEscSequence("\x1b[1;4H", "alt-shift-home")
assertEscSequence("\x1b[1;4F", "alt-shift-end")
assertEscSequence("\x1b[3;4~", "alt-shift-delete")
assertEscSequence("\x1b[5;4~", "alt-shift-page-up")
assertEscSequence("\x1b[6;4~", "alt-shift-page-down")
assertEscSequence("\x1b[1;5A", "ctrl-up")
assertEscSequence("\x1b[1;5B", "ctrl-down")
assertEscSequence("\x1b[1;5C", "ctrl-right")
assertEscSequence("\x1b[1;5D", "ctrl-left")
assertEscSequence("\x1b[1;5H", "ctrl-home")
assertEscSequence("\x1b[1;5F", "ctrl-end")
assertEscSequence("\x1b[3;5~", "ctrl-delete")
assertEscSequence("\x1b[5;5~", "ctrl-page-up")
assertEscSequence("\x1b[6;5~", "ctrl-page-down")
assertEscSequence("\x1b[1;7A", "ctrl-alt-up")
assertEscSequence("\x1b[1;7B", "ctrl-alt-down")
assertEscSequence("\x1b[1;7C", "ctrl-alt-right")
assertEscSequence("\x1b[1;7D", "ctrl-alt-left")
assertEscSequence("\x1b[1;7H", "ctrl-alt-home")
assertEscSequence("\x1b[1;7F", "ctrl-alt-end")
assertEscSequence("\x1b[3;7~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;6A", "ctrl-shift-up")
assertEscSequence("\x1b[1;6B", "ctrl-shift-down")
assertEscSequence("\x1b[1;6C", "ctrl-shift-right")
assertEscSequence("\x1b[1;6D", "ctrl-shift-left")
assertEscSequence("\x1b[1;6H", "ctrl-shift-home")
assertEscSequence("\x1b[1;6F", "ctrl-shift-end")
assertEscSequence("\x1b[3;6~", "ctrl-shift-delete")
assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up")
assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down")
assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down")
// xterm meta & mac
assertEscSequence("\x1b[1;9A", "alt-up")
assertEscSequence("\x1b[1;9B", "alt-down")
assertEscSequence("\x1b[1;9C", "alt-right")
assertEscSequence("\x1b[1;9D", "alt-left")
assertEscSequence("\x1b[1;9H", "alt-home")
assertEscSequence("\x1b[1;9F", "alt-end")
assertEscSequence("\x1b[3;9~", "alt-delete")
assertEscSequence("\x1b[5;9~", "alt-page-up")
assertEscSequence("\x1b[6;9~", "alt-page-down")
assertEscSequence("\x1b[1;10A", "alt-shift-up")
assertEscSequence("\x1b[1;10B", "alt-shift-down")
assertEscSequence("\x1b[1;10C", "alt-shift-right")
assertEscSequence("\x1b[1;10D", "alt-shift-left")
assertEscSequence("\x1b[1;10H", "alt-shift-home")
assertEscSequence("\x1b[1;10F", "alt-shift-end")
assertEscSequence("\x1b[3;10~", "alt-shift-delete")
assertEscSequence("\x1b[5;10~", "alt-shift-page-up")
assertEscSequence("\x1b[6;10~", "alt-shift-page-down")
assertEscSequence("\x1b[1;11A", "alt-up")
assertEscSequence("\x1b[1;11B", "alt-down")
assertEscSequence("\x1b[1;11C", "alt-right")
assertEscSequence("\x1b[1;11D", "alt-left")
assertEscSequence("\x1b[1;11H", "alt-home")
assertEscSequence("\x1b[1;11F", "alt-end")
assertEscSequence("\x1b[3;11~", "alt-delete")
assertEscSequence("\x1b[5;11~", "alt-page-up")
assertEscSequence("\x1b[6;11~", "alt-page-down")
assertEscSequence("\x1b[1;12A", "alt-shift-up")
assertEscSequence("\x1b[1;12B", "alt-shift-down")
assertEscSequence("\x1b[1;12C", "alt-shift-right")
assertEscSequence("\x1b[1;12D", "alt-shift-left")
assertEscSequence("\x1b[1;12H", "alt-shift-home")
assertEscSequence("\x1b[1;12F", "alt-shift-end")
assertEscSequence("\x1b[3;12~", "alt-shift-delete")
assertEscSequence("\x1b[5;12~", "alt-shift-page-up")
assertEscSequence("\x1b[6;12~", "alt-shift-page-down")
assertEscSequence("\x1b[1;13A", "ctrl-alt-up")
assertEscSequence("\x1b[1;13B", "ctrl-alt-down")
assertEscSequence("\x1b[1;13C", "ctrl-alt-right")
assertEscSequence("\x1b[1;13D", "ctrl-alt-left")
assertEscSequence("\x1b[1;13H", "ctrl-alt-home")
assertEscSequence("\x1b[1;13F", "ctrl-alt-end")
assertEscSequence("\x1b[3;13~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down")
assertEscSequence("\x1b[1;15A", "ctrl-alt-up")
assertEscSequence("\x1b[1;15B", "ctrl-alt-down")
assertEscSequence("\x1b[1;15C", "ctrl-alt-right")
assertEscSequence("\x1b[1;15D", "ctrl-alt-left")
assertEscSequence("\x1b[1;15H", "ctrl-alt-home")
assertEscSequence("\x1b[1;15F", "ctrl-alt-end")
assertEscSequence("\x1b[3;15~", "ctrl-alt-delete")
assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up")
assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down")
assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up")
assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down")
assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right")
assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left")
assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home")
assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end")
assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete")
assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up")
assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down")
// tmux & emacs
assertEscSequence("\x1bOA", "up")
assertEscSequence("\x1bOB", "down")
assertEscSequence("\x1bOC", "right")
assertEscSequence("\x1bOD", "left")
assertEscSequence("\x1bOH", "home")
assertEscSequence("\x1bOF", "end")
// rrvt
assertEscSequence("\x1b[1~", "home")
assertEscSequence("\x1b[4~", "end")
assertEscSequence("\x1b[11~", "f1")
assertEscSequence("\x1b[12~", "f2")
assertEscSequence("\x1b[13~", "f3")
assertEscSequence("\x1b[14~", "f4")
}

View File

@@ -36,8 +36,6 @@ func (p ColorPair) style() tcell.Style {
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg())) return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
} }
type Attr int32
type TcellWindow struct { type TcellWindow struct {
color bool color bool
windowType WindowType windowType WindowType
@@ -98,13 +96,6 @@ const (
Italic = Attr(tcell.AttrItalic) Italic = Attr(tcell.AttrItalic)
) )
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 7)
AttrClear = Attr(1 << 8)
BoldForce = Attr(1 << 10)
)
func (r *FullscreenRenderer) Bell() { func (r *FullscreenRenderer) Bell() {
_screen.Beep() _screen.Beep()
} }
@@ -158,15 +149,6 @@ func (c Color) Style() tcell.Color {
} }
} }
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return b | (a & BoldForce)
}
return a | b
}
// handle the following as private members of FullscreenRenderer instance // handle the following as private members of FullscreenRenderer instance
// they are declared here to prevent introducing tcell library in non-windows builds // they are declared here to prevent introducing tcell library in non-windows builds
var ( var (
@@ -353,6 +335,8 @@ func (r *FullscreenRenderer) GetChar() Event {
shift := (mods & tcell.ModShift) > 0 shift := (mods & tcell.ModShift) > 0
ctrlAlt := ctrl && alt ctrlAlt := ctrl && alt
altShift := alt && shift altShift := alt && shift
ctrlShift := ctrl && shift
ctrlAltShift := ctrl && alt && shift
keyfn := func(r rune) Event { keyfn := func(r rune) Event {
if alt { if alt {
@@ -379,8 +363,11 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyCtrlH: case tcell.KeyCtrlH:
switch ev.Rune() { switch ev.Rune() {
case 0: case 0:
if ctrlAlt {
return Event{CtrlAltBackspace, 0, nil}
}
if ctrl { if ctrl {
return Event{Backspace, 0, nil} return Event{CtrlBackspace, 0, nil}
} }
case rune(tcell.KeyCtrlH): case rune(tcell.KeyCtrlH):
switch { switch {
@@ -441,6 +428,9 @@ func (r *FullscreenRenderer) GetChar() Event {
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
// section 3: (Alt)+Backspace2 // section 3: (Alt)+Backspace2
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if ctrl {
return Event{CtrlBackspace, 0, nil}
}
if alt { if alt {
return Event{AltBackspace, 0, nil} return Event{AltBackspace, 0, nil}
} }
@@ -448,9 +438,21 @@ func (r *FullscreenRenderer) GetChar() Event {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
case tcell.KeyUp: case tcell.KeyUp:
if ctrlAltShift {
return Event{CtrlAltShiftUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftUp, 0, nil}
}
if altShift { if altShift {
return Event{AltShiftUp, 0, nil} return Event{AltShiftUp, 0, nil}
} }
if ctrl {
return Event{CtrlUp, 0, nil}
}
if shift { if shift {
return Event{ShiftUp, 0, nil} return Event{ShiftUp, 0, nil}
} }
@@ -459,9 +461,21 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
return Event{Up, 0, nil} return Event{Up, 0, nil}
case tcell.KeyDown: case tcell.KeyDown:
if ctrlAltShift {
return Event{CtrlAltShiftDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDown, 0, nil}
}
if altShift { if altShift {
return Event{AltShiftDown, 0, nil} return Event{AltShiftDown, 0, nil}
} }
if ctrl {
return Event{CtrlDown, 0, nil}
}
if shift { if shift {
return Event{ShiftDown, 0, nil} return Event{ShiftDown, 0, nil}
} }
@@ -470,9 +484,21 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
return Event{Down, 0, nil} return Event{Down, 0, nil}
case tcell.KeyLeft: case tcell.KeyLeft:
if ctrlAltShift {
return Event{CtrlAltShiftLeft, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltLeft, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftLeft, 0, nil}
}
if altShift { if altShift {
return Event{AltShiftLeft, 0, nil} return Event{AltShiftLeft, 0, nil}
} }
if ctrl {
return Event{CtrlLeft, 0, nil}
}
if shift { if shift {
return Event{ShiftLeft, 0, nil} return Event{ShiftLeft, 0, nil}
} }
@@ -481,9 +507,21 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
return Event{Left, 0, nil} return Event{Left, 0, nil}
case tcell.KeyRight: case tcell.KeyRight:
if ctrlAltShift {
return Event{CtrlAltShiftRight, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltRight, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftRight, 0, nil}
}
if altShift { if altShift {
return Event{AltShiftRight, 0, nil} return Event{AltShiftRight, 0, nil}
} }
if ctrl {
return Event{CtrlRight, 0, nil}
}
if shift { if shift {
return Event{ShiftRight, 0, nil} return Event{ShiftRight, 0, nil}
} }
@@ -496,20 +534,119 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyInsert: case tcell.KeyInsert:
return Event{Insert, 0, nil} return Event{Insert, 0, nil}
case tcell.KeyHome: case tcell.KeyHome:
if ctrlAltShift {
return Event{CtrlAltShiftHome, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltHome, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftHome, 0, nil}
}
if altShift {
return Event{AltShiftHome, 0, nil}
}
if ctrl {
return Event{CtrlHome, 0, nil}
}
if shift {
return Event{ShiftHome, 0, nil}
}
if alt {
return Event{AltHome, 0, nil}
}
return Event{Home, 0, nil} return Event{Home, 0, nil}
case tcell.KeyDelete: case tcell.KeyDelete:
if ctrlAltShift {
return Event{CtrlAltShiftDelete, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltDelete, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftDelete, 0, nil}
}
if altShift {
return Event{AltShiftDelete, 0, nil}
}
if ctrl { if ctrl {
return Event{CtrlDelete, 0, nil} return Event{CtrlDelete, 0, nil}
} }
if alt {
return Event{AltDelete, 0, nil}
}
if shift { if shift {
return Event{ShiftDelete, 0, nil} return Event{ShiftDelete, 0, nil}
} }
return Event{Delete, 0, nil} return Event{Delete, 0, nil}
case tcell.KeyEnd: case tcell.KeyEnd:
if ctrlAltShift {
return Event{CtrlAltShiftEnd, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltEnd, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftEnd, 0, nil}
}
if altShift {
return Event{AltShiftEnd, 0, nil}
}
if ctrl {
return Event{CtrlEnd, 0, nil}
}
if shift {
return Event{ShiftEnd, 0, nil}
}
if alt {
return Event{AltEnd, 0, nil}
}
return Event{End, 0, nil} return Event{End, 0, nil}
case tcell.KeyPgUp: case tcell.KeyPgUp:
if ctrlAltShift {
return Event{CtrlAltShiftPageUp, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltPageUp, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftPageUp, 0, nil}
}
if altShift {
return Event{AltShiftPageUp, 0, nil}
}
if ctrl {
return Event{CtrlPageUp, 0, nil}
}
if shift {
return Event{ShiftPageUp, 0, nil}
}
if alt {
return Event{AltPageUp, 0, nil}
}
return Event{PageUp, 0, nil} return Event{PageUp, 0, nil}
case tcell.KeyPgDn: case tcell.KeyPgDn:
if ctrlAltShift {
return Event{CtrlAltShiftPageDown, 0, nil}
}
if ctrlAlt {
return Event{CtrlAltPageDown, 0, nil}
}
if ctrlShift {
return Event{CtrlShiftPageDown, 0, nil}
}
if altShift {
return Event{AltShiftPageDown, 0, nil}
}
if ctrl {
return Event{CtrlPageDown, 0, nil}
}
if shift {
return Event{ShiftPageDown, 0, nil}
}
if alt {
return Event{AltPageDown, 0, nil}
}
return Event{PageDown, 0, nil} return Event{PageDown, 0, nil}
case tcell.KeyBacktab: case tcell.KeyBacktab:
return Event{ShiftTab, 0, nil} return Event{ShiftTab, 0, nil}
@@ -568,13 +705,13 @@ func (r *FullscreenRenderer) GetChar() Event {
func (r *FullscreenRenderer) Pause(clear bool) { func (r *FullscreenRenderer) Pause(clear bool) {
if clear { if clear {
r.Close() _screen.Suspend()
} }
} }
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) { func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
if clear { if clear {
r.initScreen() _screen.Resume()
} }
} }
@@ -600,6 +737,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
normal = ColNormal normal = ColNormal
case WindowHeader: case WindowHeader:
normal = ColHeader normal = ColHeader
case WindowFooter:
normal = ColFooter
case WindowInput: case WindowInput:
normal = ColInput normal = ColInput
case WindowPreview: case WindowPreview:
@@ -865,6 +1004,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = ColListBorder.style() style = ColListBorder.style()
case WindowHeader: case WindowHeader:
style = ColHeaderBorder.style() style = ColHeaderBorder.style()
case WindowFooter:
style = ColFooterBorder.style()
case WindowInput: case WindowInput:
style = ColInputBorder.style() style = ColInputBorder.style()
case WindowPreview: case WindowPreview:

View File

@@ -107,18 +107,20 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated {giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}}, {giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke {giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke {giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
@@ -126,9 +128,41 @@ func TestGetCharEventKey(t *testing.T) {
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right) // section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}}, {giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}}, {giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl}, wantKey{CtrlUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl}, wantKey{CtrlDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl}, wantKey{CtrlRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl}, wantKey{CtrlLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModShift}, wantKey{ShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModShift}, wantKey{ShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift}, wantKey{ShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}}, {giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModAlt}, wantKey{AltUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModAlt}, wantKey{AltRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModAlt}, wantKey{AltLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}}, {giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftLeft, 0, nil}},
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftUp, 0, nil}},
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDown, 0, nil}},
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftRight, 0, nil}},
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftLeft, 0, nil}},
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled {giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
@@ -137,6 +171,46 @@ func TestGetCharEventKey(t *testing.T) {
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12) // section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}}, {giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}}, {giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModNone}, wantKey{Home, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModNone}, wantKey{End, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModNone}, wantKey{PageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModNone}, wantKey{PageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl}, wantKey{CtrlHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl}, wantKey{CtrlEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl}, wantKey{CtrlDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl}, wantKey{CtrlPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl}, wantKey{CtrlPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModShift}, wantKey{ShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModShift}, wantKey{ShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModShift}, wantKey{ShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift}, wantKey{ShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift}, wantKey{ShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModAlt}, wantKey{AltHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModAlt}, wantKey{AltEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModAlt}, wantKey{AltPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModAlt}, wantKey{AltPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageDown, 0, nil}},
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftHome, 0, nil}},
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftEnd, 0, nil}},
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDelete, 0, nil}},
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageUp, 0, nil}},
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageDown, 0, nil}},
// section 6: (Ctrl+Alt)+'rune' // section 6: (Ctrl+Alt)+'rune'
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}}, {giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated {giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
@@ -198,6 +272,10 @@ func TestGetCharEventKey(t *testing.T) {
initialResizeAsInvalid = false initialResizeAsInvalid = false
gotEvent = r.GetChar() gotEvent = r.GetChar()
} }
if gotEvent.Type == Resize {
t.Logf("Resize swallowed")
gotEvent = r.GetChar()
}
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char) t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char) t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)

View File

@@ -48,7 +48,7 @@ func TtyIn(ttyDefault string) (*os.File, error) {
return openTtyIn(ttyDefault) return openTtyIn(ttyDefault)
} }
// TtyIn returns terminal device to write to // TtyOut returns terminal device to write to
func TtyOut(ttyDefault string) (*os.File, error) { func TtyOut(ttyDefault string) (*os.File, error) {
return openTtyOut(ttyDefault) return openTtyOut(ttyDefault)
} }

View File

@@ -8,6 +8,26 @@ import (
"github.com/rivo/uniseg" "github.com/rivo/uniseg"
) )
type Attr int32
const (
AttrUndefined = Attr(0)
AttrRegular = Attr(1 << 8)
AttrClear = Attr(1 << 9)
BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Strip = Attr(1 << 12)
)
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return (b &^ AttrRegular) | (a & BoldForce)
}
return (a &^ AttrRegular) | b
}
// Types of user action // Types of user action
// //
//go:generate stringer -type=EventType //go:generate stringer -type=EventType
@@ -44,7 +64,6 @@ const (
CtrlZ CtrlZ
Esc Esc
CtrlSpace CtrlSpace
CtrlDelete
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
CtrlBackSlash CtrlBackSlash
@@ -72,6 +91,10 @@ const (
ShiftLeft ShiftLeft
ShiftRight ShiftRight
ShiftDelete ShiftDelete
ShiftHome
ShiftEnd
ShiftPageUp
ShiftPageDown
F1 F1
F2 F2
@@ -92,15 +115,67 @@ const (
AltDown AltDown
AltLeft AltLeft
AltRight AltRight
AltDelete
AltHome
AltEnd
AltPageUp
AltPageDown
AltShiftUp AltShiftUp
AltShiftDown AltShiftDown
AltShiftLeft AltShiftLeft
AltShiftRight AltShiftRight
AltShiftDelete
AltShiftHome
AltShiftEnd
AltShiftPageUp
AltShiftPageDown
CtrlUp
CtrlDown
CtrlLeft
CtrlRight
CtrlHome
CtrlEnd
CtrlBackspace
CtrlDelete
CtrlPageUp
CtrlPageDown
Alt Alt
CtrlAlt CtrlAlt
CtrlAltUp
CtrlAltDown
CtrlAltLeft
CtrlAltRight
CtrlAltHome
CtrlAltEnd
CtrlAltBackspace
CtrlAltDelete
CtrlAltPageUp
CtrlAltPageDown
CtrlShiftUp
CtrlShiftDown
CtrlShiftLeft
CtrlShiftRight
CtrlShiftHome
CtrlShiftEnd
CtrlShiftDelete
CtrlShiftPageUp
CtrlShiftPageDown
CtrlAltShiftUp
CtrlAltShiftDown
CtrlAltShiftLeft
CtrlAltShiftRight
CtrlAltShiftHome
CtrlAltShiftEnd
CtrlAltShiftDelete
CtrlAltShiftPageUp
CtrlAltShiftPageDown
Invalid Invalid
Fatal Fatal
BracketedPasteBegin BracketedPasteBegin
@@ -132,6 +207,8 @@ const (
Jump Jump
JumpCancel JumpCancel
ClickHeader ClickHeader
ClickFooter
Multi
) )
func (t EventType) AsEvent() Event { func (t EventType) AsEvent() Event {
@@ -218,6 +295,13 @@ func (a ColorAttr) IsColorDefined() bool {
return a.Color != colUndefined return a.Color != colUndefined
} }
func (a ColorAttr) IsAttrDefined() bool {
return a.Attr != AttrUndefined
}
func (a ColorAttr) IsUndefined() bool {
return !a.IsColorDefined() && !a.IsAttrDefined()
}
func NewColorAttr() ColorAttr { func NewColorAttr() ColorAttr {
return ColorAttr{Color: colUndefined, Attr: AttrUndefined} return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
} }
@@ -246,6 +330,14 @@ const (
colMagenta colMagenta
colCyan colCyan
colWhite colWhite
colGrey
colBrightRed
colBrightGreen
colBrightYellow
colBrightBlue
colBrightMagenta
colBrightCyan
colBrightWhite
) )
type FillReturn int type FillReturn int
@@ -273,6 +365,10 @@ func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
return ColorPair{fg, bg, attr} return ColorPair{fg, bg, attr}
} }
func NoColorPair() ColorPair {
return ColorPair{-1, -1, 0}
}
func (p ColorPair) Fg() Color { func (p ColorPair) Fg() Color {
return p.fg return p.fg
} }
@@ -285,6 +381,14 @@ func (p ColorPair) Attr() Attr {
return p.attr return p.attr
} }
func (p ColorPair) IsFullBgMarker() bool {
return p.attr&FullBg > 0
}
func (p ColorPair) ShouldStripColors() bool {
return p.attr&Strip > 0
}
func (p ColorPair) HasBg() bool { func (p ColorPair) HasBg() bool {
return p.attr&Reverse == 0 && p.bg != colDefault || return p.attr&Reverse == 0 && p.bg != colDefault ||
p.attr&Reverse > 0 && p.fg != colDefault p.attr&Reverse > 0 && p.fg != colDefault
@@ -308,6 +412,18 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
return dup return dup
} }
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
dup := p
fgPair := ColorPair{fg.Color, colUndefined, fg.Attr}
return dup.Merge(fgPair)
}
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
dup := p
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
return dup.Merge(bgPair)
}
func (p ColorPair) MergeAttr(other ColorPair) ColorPair { func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
return p.WithAttr(other.attr) return p.WithAttr(other.attr)
} }
@@ -323,12 +439,15 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
type ColorTheme struct { type ColorTheme struct {
Colored bool Colored bool
Input ColorAttr Input ColorAttr
Ghost ColorAttr
Disabled ColorAttr Disabled ColorAttr
Fg ColorAttr Fg ColorAttr
Bg ColorAttr Bg ColorAttr
ListFg ColorAttr ListFg ColorAttr
ListBg ColorAttr ListBg ColorAttr
AltBg ColorAttr
Nth ColorAttr Nth ColorAttr
Nomatch ColorAttr
SelectedFg ColorAttr SelectedFg ColorAttr
SelectedBg ColorAttr SelectedBg ColorAttr
SelectedMatch ColorAttr SelectedMatch ColorAttr
@@ -351,6 +470,10 @@ type ColorTheme struct {
HeaderBg ColorAttr HeaderBg ColorAttr
HeaderBorder ColorAttr HeaderBorder ColorAttr
HeaderLabel ColorAttr HeaderLabel ColorAttr
Footer ColorAttr
FooterBg ColorAttr
FooterBorder ColorAttr
FooterLabel ColorAttr
Separator ColorAttr Separator ColorAttr
Scrollbar ColorAttr Scrollbar ColorAttr
Border ColorAttr Border ColorAttr
@@ -483,7 +606,7 @@ type BorderCharacter int
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if shape == BorderNone || shape == BorderPhantom { if shape == BorderNone || shape == BorderPhantom {
return BorderStyle{ return BorderStyle{
shape: shape, shape: BorderNone,
top: ' ', top: ' ',
bottom: ' ', bottom: ' ',
left: ' ', left: ' ',
@@ -604,6 +727,7 @@ const (
WindowPreview WindowPreview
WindowInput WindowInput
WindowHeader WindowHeader
WindowFooter
) )
type Renderer interface { type Renderer interface {
@@ -686,6 +810,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
} }
var ( var (
NoColorTheme *ColorTheme
EmptyTheme *ColorTheme
Default16 *ColorTheme Default16 *ColorTheme
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
@@ -694,9 +820,11 @@ var (
ColNormal ColorPair ColNormal ColorPair
ColInput ColorPair ColInput ColorPair
ColDisabled ColorPair ColDisabled ColorPair
ColGhost ColorPair
ColMatch ColorPair ColMatch ColorPair
ColCursor ColorPair ColCursor ColorPair
ColCursorEmpty ColorPair ColCursorEmpty ColorPair
ColCursorEmptyChar ColorPair
ColMarker ColorPair ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair ColSelectedMatch ColorPair
@@ -711,6 +839,9 @@ var (
ColHeader ColorPair ColHeader ColorPair
ColHeaderBorder ColorPair ColHeaderBorder ColorPair
ColHeaderLabel ColorPair ColHeaderLabel ColorPair
ColFooter ColorPair
ColFooterBorder ColorPair
ColFooterLabel ColorPair
ColSeparator ColorPair ColSeparator ColorPair
ColScrollbar ColorPair ColScrollbar ColorPair
ColGapLine ColorPair ColGapLine ColorPair
@@ -727,146 +858,170 @@ var (
ColInputLabel ColorPair ColInputLabel ColorPair
) )
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Colored: true,
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
ListFg: ColorAttr{colUndefined, AttrUndefined},
ListBg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
Current: ColorAttr{colUndefined, AttrUndefined},
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
ListLabel: ColorAttr{colUndefined, AttrUndefined},
ListBorder: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
Gutter: ColorAttr{colUndefined, AttrUndefined},
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
InputBg: ColorAttr{colUndefined, AttrUndefined},
InputBorder: ColorAttr{colUndefined, AttrUndefined},
InputLabel: ColorAttr{colUndefined, AttrUndefined},
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
GapLine: ColorAttr{colUndefined, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
func NoColorTheme() *ColorTheme {
return &ColorTheme{
Colored: false,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
ListFg: ColorAttr{colDefault, AttrUndefined},
ListBg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
Current: ColorAttr{colDefault, Reverse},
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
Disabled: ColorAttr{colDefault, AttrUndefined},
PreviewFg: ColorAttr{colDefault, AttrUndefined},
PreviewBg: ColorAttr{colDefault, AttrUndefined},
Gutter: ColorAttr{colDefault, AttrUndefined},
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
ListLabel: ColorAttr{colDefault, AttrUndefined},
ListBorder: ColorAttr{colDefault, AttrUndefined},
Separator: ColorAttr{colDefault, AttrUndefined},
Scrollbar: ColorAttr{colDefault, AttrUndefined},
InputBg: ColorAttr{colDefault, AttrUndefined},
InputBorder: ColorAttr{colDefault, AttrUndefined},
InputLabel: ColorAttr{colDefault, AttrUndefined},
HeaderBg: ColorAttr{colDefault, AttrUndefined},
HeaderBorder: ColorAttr{colDefault, AttrUndefined},
HeaderLabel: ColorAttr{colDefault, AttrUndefined},
GapLine: ColorAttr{colDefault, AttrUndefined},
Nth: ColorAttr{colUndefined, AttrUndefined},
}
}
func init() { func init() {
defaultColor := ColorAttr{colDefault, AttrUndefined}
undefined := ColorAttr{colUndefined, AttrUndefined}
NoColorTheme = &ColorTheme{
Colored: false,
Input: defaultColor,
Fg: defaultColor,
Bg: defaultColor,
ListFg: defaultColor,
ListBg: defaultColor,
AltBg: undefined,
SelectedFg: defaultColor,
SelectedBg: defaultColor,
SelectedMatch: defaultColor,
DarkBg: defaultColor,
Prompt: defaultColor,
Match: defaultColor,
Current: undefined,
CurrentMatch: undefined,
Spinner: defaultColor,
Info: defaultColor,
Cursor: defaultColor,
Marker: defaultColor,
Header: defaultColor,
Border: undefined,
BorderLabel: defaultColor,
Ghost: undefined,
Disabled: defaultColor,
PreviewFg: defaultColor,
PreviewBg: defaultColor,
Gutter: undefined,
PreviewBorder: defaultColor,
PreviewScrollbar: defaultColor,
PreviewLabel: defaultColor,
ListLabel: defaultColor,
ListBorder: defaultColor,
Separator: defaultColor,
Scrollbar: defaultColor,
InputBg: defaultColor,
InputBorder: defaultColor,
InputLabel: defaultColor,
HeaderBg: defaultColor,
HeaderBorder: defaultColor,
HeaderLabel: defaultColor,
FooterBg: defaultColor,
FooterBorder: defaultColor,
FooterLabel: defaultColor,
GapLine: defaultColor,
Nth: undefined,
Nomatch: undefined,
}
EmptyTheme = &ColorTheme{
Colored: true,
Input: undefined,
Fg: undefined,
Bg: undefined,
ListFg: undefined,
ListBg: undefined,
AltBg: undefined,
SelectedFg: undefined,
SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: undefined,
Prompt: undefined,
Match: undefined,
Current: undefined,
CurrentMatch: undefined,
Spinner: undefined,
Info: undefined,
Cursor: undefined,
Marker: undefined,
Header: undefined,
Footer: undefined,
Border: undefined,
BorderLabel: undefined,
ListLabel: undefined,
ListBorder: undefined,
Ghost: undefined,
Disabled: undefined,
PreviewFg: undefined,
PreviewBg: undefined,
Gutter: undefined,
PreviewBorder: undefined,
PreviewScrollbar: undefined,
PreviewLabel: undefined,
Separator: undefined,
Scrollbar: undefined,
InputBg: undefined,
InputBorder: undefined,
InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
}
Default16 = &ColorTheme{ Default16 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: defaultColor,
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: defaultColor,
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: defaultColor,
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: undefined,
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: undefined,
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, AltBg: undefined,
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: undefined,
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: undefined,
DarkBg: ColorAttr{colBlack, AttrUndefined}, SelectedMatch: undefined,
DarkBg: ColorAttr{colGrey, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined}, Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined}, Match: ColorAttr{colGreen, AttrUndefined},
Current: ColorAttr{colYellow, AttrUndefined}, Current: ColorAttr{colBrightWhite, AttrUndefined},
CurrentMatch: ColorAttr{colGreen, AttrUndefined}, CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
Spinner: ColorAttr{colGreen, AttrUndefined}, Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined}, Info: ColorAttr{colYellow, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined}, Cursor: ColorAttr{colRed, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined}, Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined}, Footer: ColorAttr{colCyan, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined}, Border: undefined,
Disabled: ColorAttr{colUndefined, AttrUndefined}, BorderLabel: defaultColor,
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Ghost: undefined,
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, Disabled: undefined,
Gutter: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: undefined,
PreviewBorder: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: undefined,
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined}, Gutter: undefined,
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: undefined,
ListLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: undefined,
ListBorder: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: undefined,
Separator: ColorAttr{colUndefined, AttrUndefined}, ListLabel: undefined,
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, ListBorder: undefined,
InputBg: ColorAttr{colUndefined, AttrUndefined}, Separator: undefined,
InputBorder: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: undefined,
InputLabel: ColorAttr{colUndefined, AttrUndefined}, InputBg: undefined,
GapLine: ColorAttr{colUndefined, AttrUndefined}, InputBorder: undefined,
Nth: ColorAttr{colUndefined, AttrUndefined}, InputLabel: undefined,
HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
} }
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: defaultColor,
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: defaultColor,
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: defaultColor,
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: undefined,
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: undefined,
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, AltBg: undefined,
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: undefined,
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: ColorAttr{236, AttrUndefined}, DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined}, Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined}, Match: ColorAttr{108, AttrUndefined},
@@ -877,35 +1032,46 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined}, Header: ColorAttr{109, AttrUndefined},
Footer: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined}, Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined}, BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Ghost: undefined,
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Disabled: undefined,
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: undefined,
Gutter: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: undefined,
PreviewBorder: ColorAttr{colUndefined, AttrUndefined}, Gutter: undefined,
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: undefined,
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: undefined,
ListLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: undefined,
ListBorder: ColorAttr{colUndefined, AttrUndefined}, ListLabel: undefined,
Separator: ColorAttr{colUndefined, AttrUndefined}, ListBorder: undefined,
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Separator: undefined,
InputBg: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: undefined,
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBg: undefined,
InputLabel: ColorAttr{colUndefined, AttrUndefined}, InputBorder: undefined,
GapLine: ColorAttr{colUndefined, AttrUndefined}, InputLabel: undefined,
Nth: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: undefined,
HeaderBorder: undefined,
HeaderLabel: undefined,
FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
} }
Light256 = &ColorTheme{ Light256 = &ColorTheme{
Colored: true, Colored: true,
Input: ColorAttr{colDefault, AttrUndefined}, Input: defaultColor,
Fg: ColorAttr{colDefault, AttrUndefined}, Fg: defaultColor,
Bg: ColorAttr{colDefault, AttrUndefined}, Bg: defaultColor,
ListFg: ColorAttr{colUndefined, AttrUndefined}, ListFg: undefined,
ListBg: ColorAttr{colUndefined, AttrUndefined}, ListBg: undefined,
SelectedFg: ColorAttr{colUndefined, AttrUndefined}, AltBg: undefined,
SelectedBg: ColorAttr{colUndefined, AttrUndefined}, SelectedFg: undefined,
SelectedMatch: ColorAttr{colUndefined, AttrUndefined}, SelectedBg: undefined,
SelectedMatch: undefined,
DarkBg: ColorAttr{251, AttrUndefined}, DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined}, Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined}, Match: ColorAttr{66, AttrUndefined},
@@ -916,35 +1082,57 @@ func init() {
Cursor: ColorAttr{161, AttrUndefined}, Cursor: ColorAttr{161, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined}, Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined}, Header: ColorAttr{31, AttrUndefined},
Footer: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined}, Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined}, BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined}, Ghost: undefined,
PreviewFg: ColorAttr{colUndefined, AttrUndefined}, Disabled: undefined,
PreviewBg: ColorAttr{colUndefined, AttrUndefined}, PreviewFg: undefined,
Gutter: ColorAttr{colUndefined, AttrUndefined}, PreviewBg: undefined,
PreviewBorder: ColorAttr{colUndefined, AttrUndefined}, Gutter: undefined,
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined}, PreviewBorder: undefined,
PreviewLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewScrollbar: undefined,
ListLabel: ColorAttr{colUndefined, AttrUndefined}, PreviewLabel: undefined,
ListBorder: ColorAttr{colUndefined, AttrUndefined}, ListLabel: undefined,
Separator: ColorAttr{colUndefined, AttrUndefined}, ListBorder: undefined,
Scrollbar: ColorAttr{colUndefined, AttrUndefined}, Separator: undefined,
InputBg: ColorAttr{colUndefined, AttrUndefined}, Scrollbar: undefined,
InputBorder: ColorAttr{colUndefined, AttrUndefined}, InputBg: undefined,
InputLabel: ColorAttr{colUndefined, AttrUndefined}, InputBorder: undefined,
HeaderBg: ColorAttr{colUndefined, AttrUndefined}, InputLabel: undefined,
HeaderBorder: ColorAttr{colUndefined, AttrUndefined}, HeaderBg: undefined,
HeaderLabel: ColorAttr{colUndefined, AttrUndefined}, HeaderBorder: undefined,
GapLine: ColorAttr{colUndefined, AttrUndefined}, HeaderLabel: undefined,
Nth: ColorAttr{colUndefined, AttrUndefined}, FooterBg: undefined,
FooterBorder: undefined,
FooterLabel: undefined,
GapLine: undefined,
Nth: undefined,
Nomatch: undefined,
} }
} }
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) { func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
if forceBlack { if forceBlack {
theme.Bg = ColorAttr{colBlack, AttrUndefined} theme.Bg = ColorAttr{colBlack, AttrUndefined}
} }
if boldify {
boldify := func(c ColorAttr) ColorAttr {
dup := c
if (c.Attr & AttrRegular) == 0 {
dup.Attr |= BoldForce
}
return dup
}
theme.Current = boldify(theme.Current)
theme.CurrentMatch = boldify(theme.CurrentMatch)
theme.Prompt = boldify(theme.Prompt)
theme.Input = boldify(theme.Input)
theme.Cursor = boldify(theme.Cursor)
theme.Spinner = boldify(theme.Spinner)
}
o := func(a ColorAttr, b ColorAttr) ColorAttr { o := func(a ColorAttr, b ColorAttr) ColorAttr {
c := a c := a
if b.Color != colUndefined { if b.Color != colUndefined {
@@ -960,17 +1148,36 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.Bg = o(baseTheme.Bg, theme.Bg) theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt) theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match) match := theme.Match
if !baseTheme.Colored && match.IsUndefined() {
match.Attr = Underline
}
theme.Match = o(baseTheme.Match, match)
// Inherit from 'fg', so that we don't have to write 'current-fg:dim' // Inherit from 'fg', so that we don't have to write 'current-fg:dim'
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular // e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
theme.Current = theme.Fg.Merge(o(baseTheme.Current, theme.Current)) current := theme.Current
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) if !baseTheme.Colored && current.IsUndefined() {
current.Attr = Reverse
}
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
currentMatch := theme.CurrentMatch
if !baseTheme.Colored && currentMatch.IsUndefined() {
currentMatch.Attr = Reverse | Underline
}
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
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.Marker = o(baseTheme.Marker, theme.Marker) 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.Footer = o(baseTheme.Footer, theme.Footer)
// If border color is undefined, set it to default color with dim attribute.
border := theme.Border
if baseTheme.Border.IsUndefined() && border.IsUndefined() {
border.Attr = Dim
}
theme.Border = o(baseTheme.Border, border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
undefined := NewColorAttr() undefined := NewColorAttr()
@@ -983,8 +1190,23 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg) theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg) theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch) theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
ghost := theme.Ghost
if ghost.IsUndefined() {
ghost.Attr = Dim
} else if ghost.IsColorDefined() && !ghost.IsAttrDefined() {
// Don't want to inherit 'bold' from 'input'
ghost.Attr = AttrRegular
}
theme.Ghost = o(theme.Input, ghost)
theme.Disabled = o(theme.Input, theme.Disabled) theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
// Use dim gutter on non-colored themes if undefined
gutter := theme.Gutter
if !baseTheme.Colored && gutter.IsUndefined() {
gutter.Attr = Dim
}
theme.Gutter = o(theme.DarkBg, gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg) theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg) theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel) theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
@@ -1022,6 +1244,14 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder) theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel) theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
theme.FooterBg = o(theme.Bg, theme.FooterBg)
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
if theme.Nomatch.IsUndefined() {
theme.Nomatch.Attr = Dim
}
initPalette(theme) initPalette(theme)
} }
@@ -1039,15 +1269,17 @@ func initPalette(theme *ColorTheme) {
ColNormal = pair(theme.ListFg, theme.ListBg) ColNormal = pair(theme.ListFg, theme.ListBg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg) ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.InputBg) ColInput = pair(theme.Input, theme.InputBg)
ColDisabled = pair(theme.Disabled, theme.ListBg) ColGhost = pair(theme.Ghost, theme.InputBg)
ColDisabled = pair(theme.Disabled, theme.InputBg)
ColMatch = pair(theme.Match, theme.ListBg) ColMatch = pair(theme.Match, theme.ListBg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg) 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)
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
if theme.SelectedBg.Color != theme.ListBg.Color { if theme.SelectedBg.Color != theme.ListBg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg) ColMarker = pair(theme.Marker, theme.SelectedBg)
} else { } else {
ColMarker = pair(theme.Marker, theme.Gutter) ColMarker = pair(theme.Marker, theme.ListBg)
} }
ColCurrent = pair(theme.Current, theme.DarkBg) ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
@@ -1074,6 +1306,9 @@ func initPalette(theme *ColorTheme) {
ColHeader = pair(theme.Header, theme.HeaderBg) ColHeader = pair(theme.Header, theme.HeaderBg)
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg) ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg) ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg)
ColFooter = pair(theme.Footer, theme.FooterBg)
ColFooterBorder = pair(theme.FooterBorder, theme.FooterBg)
ColFooterLabel = pair(theme.FooterLabel, theme.FooterBg)
} }
func runeWidth(r rune) int { func runeWidth(r rune) int {

View File

@@ -184,9 +184,10 @@ func (chars *Chars) TrailingWhitespaces() int {
return whitespaces return whitespaces
} }
func (chars *Chars) TrimTrailingWhitespaces() { func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
whitespaces := chars.TrailingWhitespaces() whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] end := len(chars.slice) - whitespaces
chars.slice = chars.slice[0:Max(end, maxIndex)]
} }
func (chars *Chars) TrimSuffix(runes []rune) { func (chars *Chars) TrimSuffix(runes []rune) {

View File

@@ -0,0 +1,39 @@
package util
import "sync"
// ConcurrentSet is a thread-safe set implementation.
type ConcurrentSet[T comparable] struct {
lock sync.RWMutex
items map[T]struct{}
}
// NewConcurrentSet creates a new ConcurrentSet.
func NewConcurrentSet[T comparable]() *ConcurrentSet[T] {
return &ConcurrentSet[T]{
items: make(map[T]struct{}),
}
}
// Add adds an item to the set.
func (s *ConcurrentSet[T]) Add(item T) {
s.lock.Lock()
defer s.lock.Unlock()
s.items[item] = struct{}{}
}
// Remove removes an item from the set.
func (s *ConcurrentSet[T]) Remove(item T) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.items, item)
}
// ForEach iterates over each item in the set and applies the provided function.
func (s *ConcurrentSet[T]) ForEach(fn func(item T)) {
s.lock.RLock()
defer s.lock.RUnlock()
for item := range s.items {
fn(item)
}
}

View File

@@ -97,24 +97,12 @@ func Min32(first int32, second int32) int32 {
// Constrain32 limits the given 32-bit integer with the upper and lower bounds // Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 { func Constrain32(val int32, min int32, max int32) int32 {
if val < min { return Max32(Min32(val, max), min)
return min
}
if val > max {
return max
}
return val
} }
// Constrain limits the given integer with the upper and lower bounds // Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int { func Constrain(val int, min int, max int) int {
if val < min { return Max(Min(val, max), min)
return min
}
if val > max {
return max
}
return val
} }
func AsUint16(val int) uint16 { func AsUint16(val int) uint16 {

View File

@@ -24,7 +24,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__) FILE = File.expand_path(__FILE__)
BASE = File.expand_path('../..', __dir__) BASE = File.expand_path('../..', __dir__)
Dir.chdir(BASE) Dir.chdir(BASE)
FZF = "FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer \\> --marker \\>\" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf".freeze FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
def wait(timeout = DEFAULT_TIMEOUT) def wait(timeout = DEFAULT_TIMEOUT)
since = Time.now since = Time.now

View File

@@ -108,6 +108,40 @@ class TestCore < TestInteractive
assert_equal %w[3 2 5 6 8 7], fzf_output_lines assert_equal %w[3 2 5 6 8 7], fzf_output_lines
end end
def test_subword_forward
tmux.send_keys "#{FZF} --bind K:kill-subword,F:forward-subword -q 'foo bar foo-bar fooFooBar'", :Enter, :Home
tmux.until { |lines| assert_equal '> foo bar foo-bar fooFooBar', lines.last }
tmux.send_keys 'F', :Delete
tmux.until { |lines| assert_equal '> foobar foo-bar fooFooBar', lines.last }
tmux.send_keys 'K'
tmux.until { |lines| assert_equal '> foo foo-bar fooFooBar', lines.last }
tmux.send_keys 'F', 'K'
tmux.until { |lines| assert_equal '> foo foo fooFooBar', lines.last }
tmux.send_keys 'F', 'F', 'K'
tmux.until { |lines| assert_equal '> foo foo fooFoo', lines.last }
end
def test_subword_backward
tmux.send_keys "#{FZF} --bind K:backward-kill-subword,B:backward-subword -q 'foo bar foo-bar fooBar'", :Enter
tmux.until { |lines| assert_equal '> foo bar foo-bar fooBar', lines.last }
tmux.send_keys 'B', :BSpace
tmux.until { |lines| assert_equal '> foo bar foo-bar foBar', lines.last }
tmux.send_keys 'K'
tmux.until { |lines| assert_equal '> foo bar foo-bar Bar', lines.last }
tmux.send_keys 'B', :BSpace
tmux.until { |lines| assert_equal '> foo bar foobar Bar', lines.last }
tmux.send_keys 'B', 'B', :BSpace
tmux.until { |lines| assert_equal '> foobar foobar Bar', lines.last }
end
def test_multi_max def test_multi_max
tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter tmux.send_keys "seq 1 10 | #{FZF} -m 3 --bind A:select-all,T:toggle-all --preview 'echo [{+}]/{}'", :Enter
@@ -1415,6 +1449,11 @@ class TestCore < TestInteractive
tmux.until { assert_match(%r{ --1/10000/10000-- *$}, it[-1]) } tmux.until { assert_match(%r{ --1/10000/10000-- *$}, it[-1]) }
end end
def test_info_command_inline_right_no_ansi
tmux.send_keys(%(seq 10000 | #{FZF} --info-command 'echo -e "--$FZF_POS/$FZF_INFO--"' --info inline-right), :Enter)
tmux.until { assert_match(%r{ --1/10000/10000-- *$}, it[-1]) }
end
def test_info_command_and_focus def test_info_command_and_focus
tmux.send_keys(%(seq 100 | #{FZF} --separator x --info-command 'echo $FZF_POS' --bind focus:clear-query), :Enter) tmux.send_keys(%(seq 100 | #{FZF} --separator x --info-command 'echo $FZF_POS' --bind focus:clear-query), :Enter)
tmux.until { assert_match(/^ 1 xx/, it[-2]) } tmux.until { assert_match(/^ 1 xx/, it[-2]) }
@@ -1632,15 +1671,18 @@ class TestCore < TestInteractive
end end
def test_env_vars def test_env_vars
def to_vars(lines) def env_vars
lines.select { it.start_with?('FZF_') }.to_h do return {} unless File.exist?(tempname)
key, val = it.split('=', 2)
File.readlines(tempname).select { it.start_with?('FZF_') }.to_h do
key, val = it.chomp.split('=', 2)
[key.to_sym, val] [key.to_sym, val]
end end
end end
tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window up,99%,noborder --preview 'env | grep ^FZF_ | sort' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter tmux.send_keys %(seq 100 | #{FZF} --multi --reverse --preview-window 0 --preview 'env | grep ^FZF_ | sort > #{tempname}' --no-input --bind enter:show-input+refresh-preview,space:disable-search+refresh-preview), :Enter
expected = { expected = {
FZF_DIRECTION: 'down',
FZF_TOTAL_COUNT: '100', FZF_TOTAL_COUNT: '100',
FZF_MATCH_COUNT: '100', FZF_MATCH_COUNT: '100',
FZF_SELECT_COUNT: '0', FZF_SELECT_COUNT: '0',
@@ -1648,31 +1690,32 @@ class TestCore < TestInteractive
FZF_KEY: '', FZF_KEY: '',
FZF_POS: '1', FZF_POS: '1',
FZF_QUERY: '', FZF_QUERY: '',
FZF_PROMPT: '>', FZF_POINTER: '>',
FZF_PROMPT: '> ',
FZF_INPUT_STATE: 'hidden' FZF_INPUT_STATE: 'hidden'
} }
tmux.until do |lines| tmux.until do
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do
expected.merge!(FZF_INPUT_STATE: 'enabled', FZF_ACTION: 'show-input', FZF_KEY: 'enter') expected.merge!(FZF_INPUT_STATE: 'enabled', FZF_ACTION: 'show-input', FZF_KEY: 'enter')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.until do |lines| tmux.until do
expected.merge!(FZF_ACTION: 'toggle-down', FZF_KEY: 'tab', FZF_POS: '3', FZF_SELECT_COUNT: '2') expected.merge!(FZF_ACTION: 'toggle-down', FZF_KEY: 'tab', FZF_POS: '3', FZF_SELECT_COUNT: '2')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys '99' tmux.send_keys '99'
tmux.until do |lines| tmux.until do
expected.merge!(FZF_ACTION: 'char', FZF_KEY: '9', FZF_QUERY: '99', FZF_MATCH_COUNT: '1', FZF_POS: '1') expected.merge!(FZF_ACTION: 'char', FZF_KEY: '9', FZF_QUERY: '99', FZF_MATCH_COUNT: '1', FZF_POS: '1')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
tmux.send_keys :Space tmux.send_keys :Space
tmux.until do |lines| tmux.until do
expected.merge!(FZF_INPUT_STATE: 'disabled', FZF_ACTION: 'disable-search', FZF_KEY: 'space') expected.merge!(FZF_INPUT_STATE: 'disabled', FZF_ACTION: 'disable-search', FZF_KEY: 'space')
assert_equal expected, to_vars(lines).slice(*expected.keys) assert_equal expected, env_vars.slice(*expected.keys)
end end
end end
@@ -1856,9 +1899,9 @@ class TestCore < TestInteractive
line = nil line = nil
tmux.until { |lines| line = lines.index('> 1') } tmux.until { |lines| line = lines.index('> 1') }
tmux.send_keys :PgDn tmux.send_keys :PgDn
tmux.until { |lines| assert_includes lines[line + 4], "> 5" } tmux.until { |lines| assert_includes lines[line + 4], '> 5' }
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert_includes lines[line + 2], "> 5" } tmux.until { |lines| assert_includes lines[line + 2], '> 5' }
end end
def test_no_input_query def test_no_input_query
@@ -1927,7 +1970,10 @@ class TestCore < TestInteractive
def test_change_header_on_header_window def test_change_header_on_header_window
tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter tmux.send_keys %(seq 100 | #{FZF} --list-border --input-border --bind 'start:change-header(foo),space:change-header(bar)'), :Enter
tmux.until { |lines| assert lines.any_include?('foo') } tmux.until do |lines|
assert lines.any_include?('100/100')
assert lines.any_include?('foo')
end
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('bar') } tmux.until { |lines| assert lines.any_include?('bar') }
end end
@@ -1936,4 +1982,122 @@ class TestCore < TestInteractive
tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter tmux.send_keys %(echo -en "foo\n" | fzf --read0 --no-multi-line), :Enter
tmux.until { |lines| assert_includes lines, '> foo␊' } tmux.until { |lines| assert_includes lines, '> foo␊' }
end end
def test_async_transform
time = Time.now
tmux.send_keys %(
seq 100 | #{FZF} --style full --border --preview : \
--bind 'focus:bg-transform-header(sleep 0.5; echo th.)' \
--bind 'focus:+bg-transform-footer(sleep 0.5; echo tf.)' \
--bind 'focus:+bg-transform-border-label(sleep 0.5; echo tbl.)' \
--bind "focus:+bg-transform-preview-label(sleep 0.5; echo tpl.)" \
--bind 'focus:+bg-transform-input-label(sleep 0.5; echo til.)' \
--bind 'focus:+bg-transform-list-label(sleep 0.5; echo tll.)' \
--bind 'focus:+bg-transform-header-label(sleep 0.5; echo thl.)' \
--bind 'focus:+bg-transform-footer-label(sleep 0.5; echo tfl.)' \
--bind 'focus:+bg-transform-prompt(sleep 0.5; echo tp.)' \
--bind 'focus:+bg-transform-ghost(sleep 0.5; echo tg.)'
).strip, :Enter
tmux.until do |lines|
assert lines.any_include?('100/100')
%w[th tf tbl tpl til tll thl tfl tp tg].each do
assert lines.any_include?("#{it}.")
end
end
elapsed = Time.now - time
assert_operator elapsed, :<, 2
end
def test_bg_cancel
tmux.send_keys %(seq 0 1 | #{FZF} --bind 'space:bg-cancel+bg-transform-header(sleep {}; echo [{}])'), :Enter
tmux.until { assert_equal 2, it.match_count }
tmux.send_keys '1'
tmux.until { assert_equal 1, it.match_count }
tmux.send_keys :Space
tmux.send_keys :BSpace
tmux.until { assert_equal 2, it.match_count }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('[0]') }
sleep(2)
tmux.until do |lines|
assert lines.any_include?('[0]')
refute lines.any_include?('[1]')
end
end
def test_render_order
tmux.send_keys %(seq 100 | #{FZF} --bind='focus:preview(echo boom)+change-footer(bam)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { assert it.any_include?('boom') }
tmux.until { assert it.any_include?('bam') }
end
def test_multi_event
tmux.send_keys %(seq 100 | #{FZF} --multi --bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.send_keys :Tab
tmux.until { assert_equal 1, it.select_count }
tmux.until { assert it.any_include?('Selected 1 item(s)') }
tmux.send_keys :Tab
tmux.until { assert_equal 0, it.select_count }
tmux.until { refute it.any_include?('Selected') }
end
def test_preserve_selection_on_revision_bump
tmux.send_keys %(seq 100 | #{FZF} --multi --sync --query "'1" --bind 'a:select-all+change-header(pressed a),b:change-header(pressed b)+change-nth(1),c:exclude'), :Enter
tmux.until do
assert_equal 20, it.match_count
assert_equal 0, it.select_count
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
end
tmux.send_keys :b
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
refute it.any_include?('pressed a')
assert it.any_include?('pressed b')
end
tmux.send_keys :a
tmux.until do
assert_equal 20, it.match_count
assert_equal 20, it.select_count
assert it.any_include?('pressed a')
refute it.any_include?('pressed b')
end
tmux.send_keys :c
tmux.until do
assert_equal 19, it.match_count
assert_equal 19, it.select_count
end
end
def test_trigger
tmux.send_keys %(seq 100 | #{FZF} --bind 'a:up+trigger(a),b:trigger(a,a,b,a)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { |lines| assert_includes lines, '> 1' }
tmux.send_keys :a
tmux.until { |lines| assert_includes lines, '> 3' }
tmux.send_keys :b
tmux.until { |lines| assert_includes lines, '> 9' }
end
def test_change_nth_unset_default
tmux.send_keys %(echo foo bar | #{FZF} --nth 2 --query fb --bind space:change-nth:), :Enter
tmux.until do
assert_equal 1, it.item_count
assert_equal 0, it.match_count
end
tmux.send_keys :Space
tmux.until do
assert_equal 1, it.item_count
assert_equal 1, it.match_count
end
end
end end

View File

@@ -403,7 +403,7 @@ class TestExec < TestInteractive
end end
def test_become def test_become
tmux.send_keys "seq 100 | #{FZF} --bind 'enter:become:seq {} | #{FZF}'", :Enter tmux.send_keys "seq 100 | fzf --bind 'enter:become:seq {} | fzf'", :Enter
tmux.until { |lines| assert_equal 100, lines.match_count } tmux.until { |lines| assert_equal 100, lines.match_count }
tmux.send_keys 999 tmux.send_keys 999
tmux.until { |lines| assert_equal 0, lines.match_count } tmux.until { |lines| assert_equal 0, lines.match_count }

View File

@@ -304,11 +304,11 @@ class TestFilter < TestBase
def test_boundary_match def test_boundary_match
# Underscore boundaries should be ranked lower # Underscore boundaries should be ranked lower
{ {
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_], default: [' xyz '] + %w[/xyz/ [xyz] -xyz- -xyz_ _xyz- _xyz_],
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_], path: ['/xyz/', ' xyz '] + %w[[xyz] -xyz- -xyz_ _xyz- _xyz_],
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_] history: ['[xyz]', '-xyz-', ' xyz '] + %w[/xyz/ -xyz_ _xyz- _xyz_]
}.each do |scheme, expected| }.each do |scheme, expected|
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true) result = `printf -- 'xxyzx\n-xxyz\nxyzx-\n_xyz_\n_xyz-\n-xyz_\n[xyz]\n-xyz-\n xyz \n/xyz/\n' | #{FZF} -f"'xyz'" --scheme=#{scheme}`.lines(chomp: true)
assert_equal expected, result assert_equal expected, result
end end
end end

View File

@@ -39,11 +39,11 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter
block = <<~OUTPUT block = <<~OUTPUT
> 4 > 4
997/997
>
3 3
2 2
1 1
997/997
>
foobar foobar
OUTPUT OUTPUT
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
@@ -53,10 +53,10 @@ class TestLayout < TestInteractive
tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter
block = <<~OUTPUT block = <<~OUTPUT
foobar foobar
> < 997/997
1 1
2 2
3 3
> < 997/997
> 4 > 4
OUTPUT OUTPUT
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
@@ -148,10 +148,10 @@ class TestLayout < TestInteractive
4 4
> 3 > 3
2/2
>
2 2
1 1
2/2
>
foo foo
OUTPUT OUTPUT
@@ -178,8 +178,8 @@ class TestLayout < TestInteractive
tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border rounded', :Enter
expected = <<~OUTPUT expected = <<~OUTPUT
3 3
2 2
> 1 > 1
> < 3/3 > < 3/3
@@ -197,8 +197,8 @@ class TestLayout < TestInteractive
3 3
2 2
> 1 > 1
> < 3/3 > < 3/3
@@ -247,7 +247,7 @@ class TestLayout < TestInteractive
tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border rounded', :Enter
expected = <<~OUTPUT expected = <<~OUTPUT
2 2
> 1 > 1
> < 100/100 > < 100/100
@@ -275,12 +275,12 @@ class TestLayout < TestInteractive
def test_fzf_multi_line def test_fzf_multi_line
tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter tmux.send_keys %[(echo -en '0\\0'; echo -en '1\\n2\\0'; seq 1000) | fzf --read0 --multi --bind load:select-all --border rounded], :Enter
block = <<~BLOCK block = <<~BLOCK
998 998
999 999
1000 1000
1 1
2 2
>>0 >>0
3/3 (3) 3/3 (3)
> >
@@ -312,11 +312,11 @@ class TestLayout < TestInteractive
> >
3/3 (3) 3/3 (3)
>>0 >>0
1 1
2 2
1 1
2 2
3 3
BLOCK BLOCK
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
end end
@@ -609,11 +609,11 @@ class TestLayout < TestInteractive
4 4
> 3 > 3
2
1
98/98 98/98
> >
2
1
hello hello
BLOCK BLOCK
@@ -666,12 +666,12 @@ class TestLayout < TestInteractive
4 4
> 3 > 3
98/98
>
2 2
1 1
98/98
>
BLOCK BLOCK
tmux.until { assert_block(block1, it) } tmux.until { assert_block(block1, it) }
@@ -979,6 +979,128 @@ class TestLayout < TestInteractive
end end
end end
def test_layout_default_with_footer
prefix = %[
seq 3 | #{FZF} --no-list-border --height ~100% \
--border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \
--header-label HEAD --header-label-pos 3:bottom \
--bind 'space:transform-footer-label(echo foot)+change-header-label(head)'
].strip + ' '
suffixes = [
%(),
%[--header "$(seq 101 102)"],
%[--header "$(seq 101 102)" --header-first],
%[--header "$(seq 101 102)" --header-lines 2],
%[--header "$(seq 101 102)" --header-lines 2 --header-first],
%[--header "$(seq 101 102)" --header-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-first],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --no-header-lines-border],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border none],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input],
%[--header "$(seq 101 102)" --footer-border sharp --input-border line],
%[--header "$(seq 101 102)" --style full:sharp --header-first]
]
output = <<~BLOCK
201 201 201 201 201 201 201 201 201 201 201 201 201 FOOT FOOT
202 202 202 202 202 202 202 202 202 202 202 202 202 201 201
FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT 202 202
3 3 3 > 3 > 3 3 3 > 3 > 3 > 3 > 3 > 3 > 3
2 2 2 2 2 2 2 2 3
> 1 > 1 > 1 1 1 > 1 > 1 2 2 1 2 2 2 2 3
3/3 101 3/3 101 1/1 3/3 1 1 1 1 1 > 1 2
> 102 > 102 > 101 > 101 101 101 101 > 1
3/3 101 1/1 101 102 102 102 102 102
> 102 > 102 HEAD 101 HEAD HEAD HEAD 101 1/1 101
3/3 102 1/1 1/1 1/1 102 > 102 3/3 >
> HEAD > > > HEAD HEAD >
1/1
> 101 101
102 102
HEAD HEAD
BLOCK
expects = []
output.each_line.first.scan(/\S+/) do
offset = Regexp.last_match.offset(0)
expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n")
end
suffixes.zip(expects).each do |suffix, block|
tmux.send_keys(prefix + suffix, :Enter)
tmux.until { assert_block(block, it) }
tmux.send_keys :Space
tmux.until { assert_block(block.downcase, it) }
teardown
setup
end
end
def test_layout_reverse_list_with_footer
prefix = %[
seq 3 | #{FZF} --layout reverse-list --no-list-border --height ~100% \
--border sharp --footer "$(seq 201 202)" --footer-label FOOT --footer-label-pos 3 \
--header-label HEAD --header-label-pos 3:bottom \
--bind 'space:transform-footer-label(echo foot)+change-header-label(head)'
].strip + ' '
suffixes = [
%(),
%[--header "$(seq 101 102)"],
%[--header "$(seq 101 102)" --header-first],
%[--header "$(seq 101 102)" --header-lines 2],
%[--header "$(seq 101 102)" --header-lines 2 --header-first],
%[--header "$(seq 101 102)" --header-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-first],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --input-border sharp],
%[--header "$(seq 101 102)" --header-border sharp --header-lines 2 --header-lines-border sharp --header-first --no-input],
%[--header "$(seq 101 102)" --footer-border sharp --input-border line],
%[--header "$(seq 101 102)" --style full:sharp --header-first]
]
output = <<~BLOCK
201 201 201 201 201 201 201 201 201 201 201 FOOT FOOT
202 202 202 202 202 202 202 202 202 202 202 201 201
FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT FOOT 202 202
> 1 > 1 > 1 1 1 > 1 > 1 1
2 2 2 2 2 2 2 2 1 1 1 > 1
3 3 3 > 3 > 3 3 3 > 3 2 2 2 2 > 1
3/3 101 3/3 101 1/1 3/3 3 2
> 102 > 102 > 101 > 101 > 3 > 3 > 3 101 3
3/3 101 1/1 101 102 102 102
> 102 > 102 HEAD 101 HEAD 101 1/1 101
3/3 102 1/1 102 > 102 3/3 >
> HEAD > HEAD HEAD >
1/1
> 101 101
102 102
HEAD HEAD
BLOCK
expects = []
output.each_line.first.scan(/\S+/) do
offset = Regexp.last_match.offset(0)
expects << output.lines.filter_map { it[offset[0]...offset[1]]&.strip }.take_while { !it.empty? }.join("\n")
end
suffixes.zip(expects).each do |suffix, block|
tmux.send_keys(prefix + suffix, :Enter)
tmux.until { assert_block(block, it) }
tmux.send_keys :Space
tmux.until { assert_block(block.downcase, it) }
teardown
setup
end
end
def test_change_header_and_label_at_once def test_change_header_and_label_at_once
tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter tmux.send_keys %(seq 10 | #{FZF} --border sharp --header-border sharp --header-label-pos 3 --bind 'focus:change-header(header)+change-header-label(label)'), :Enter
block = <<~BLOCK block = <<~BLOCK
@@ -992,6 +1114,36 @@ class TestLayout < TestInteractive
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
end end
def test_label_truncation
command = <<~CMD
seq 10 | #{FZF} --style full --border --header-lines=1 --preview ':' \\
--border-label "#{'b' * 1000}" \\
--preview-label "#{'p' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--header-label "#{'h' * 1000}" \\
--input-label "#{'i' * 1000}" \\
--list-label "#{'l' * 1000}"
CMD
writelines(command.lines.map(&:chomp))
tmux.send_keys("sh #{tempname}", :Enter)
tmux.until do |lines|
text = lines.join
assert_includes text, 'b··'
assert_includes text, 'l··p'
assert_includes text, 'p··'
assert_includes text, 'h··'
assert_includes text, 'i··'
end
end
def test_separator_no_ellipsis
tmux.send_keys %(seq 10 | #{FZF} --separator "$(seq 1000 | tr '\\n' ' ')"), :Enter
tmux.until do |lines|
assert_equal 10, lines.match_count
refute_includes lines.join, '··'
end
end
def test_header_border_no_pointer_and_marker def test_header_border_no_pointer_and_marker
tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter tmux.send_keys %(seq 10 | #{FZF} --header-lines 1 --header-border sharp --no-list-border --pointer '' --marker ''), :Enter
block = <<~BLOCK block = <<~BLOCK
@@ -1003,4 +1155,138 @@ class TestLayout < TestInteractive
BLOCK BLOCK
tmux.until { assert_block(block, it) } tmux.until { assert_block(block, it) }
end end
def test_gutter_default
tmux.send_keys %(seq 10 | fzf), :Enter
block = <<~BLOCK
3
2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_gutter_default_no_unicode
tmux.send_keys %(seq 10 | fzf --no-unicode), :Enter
block = <<~BLOCK
3
2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
def test_gutter_custom
tmux.send_keys %(seq 10 | fzf --gutter x), :Enter
block = <<~BLOCK
x 3
x 2
> 1
10/10
>
BLOCK
tmux.until { assert_block(block, it) }
end
# https://github.com/junegunn/fzf/issues/4537
def test_no_scrollbar_preview_toggle
x = 'x' * 300
y = 'y' * 300
tmux.send_keys %(yes #{x} | head -1000 | fzf --bind 'tab:toggle-preview' --border --no-scrollbar --preview 'echo #{y}' --preview-window 'border-left'), :Enter
# │ ▌ xxxxxxxx·· │ yyyyyyyy│
tmux.until do |lines|
lines.any? { it.match?(/x·· │ y+│$/) }
end
tmux.send_keys :Tab
# │ ▌ xxxxxxxx·· │
tmux.until do |lines|
lines.none? { it.match?(/x··y│$/) }
end
tmux.send_keys :Tab
tmux.until do |lines|
lines.any? { it.match?(/x·· │ y+│$/) }
end
end
def test_combinations
skip unless ENV['LONGTEST']
base = [
'--pointer=@',
'--exact',
'--query=123',
'--header="$(seq 101 103)"',
'--header-lines=3',
'--footer "$(seq 201 203)"',
'--preview "echo foobar"'
]
options = [
['--separator==', '--no-separator'],
['--info=default', '--info=inline', '--info=inline-right'],
['--no-input-border', '--input-border'],
['--no-header-border', '--header-border=none', '--header-border'],
['--no-header-lines-border', '--header-lines-border'],
['--no-footer-border', '--footer-border'],
['--no-list-border', '--list-border'],
['--preview-window=right', '--preview-window=up', '--preview-window=down', '--preview-window=left'],
['--header-first', '--no-header-first'],
['--layout=default', '--layout=reverse', '--layout=reverse-list']
]
# Combination of all options
combinations = options[0].product(*options.drop(1))
combinations.each_with_index do |combination, index|
opts = base + combination
command = %(seq 1001 2000 | #{FZF} #{opts.join(' ')})
puts "# #{index + 1}/#{combinations.length}\n#{command}"
tmux.send_keys command, :Enter
tmux.until do |lines|
layout = combination.find { it.start_with?('--layout=') }.split('=').last
header_first = combination.include?('--header-first')
# Input
input = lines.index { it.include?('> 123') }
assert(input)
# Info
info = lines.index { it.include?('11/997') }
assert(info)
assert(layout == 'reverse' ? input <= info : input >= info)
# List
item1 = lines.index { it.include?('1230') }
item2 = lines.index { it.include?('1231') }
assert_equal(item1, layout == 'default' ? item2 + 1 : item2 - 1)
# Preview
assert(lines.any? { it.include?('foobar') })
# Header
header1 = lines.index { it.include?('101') }
header2 = lines.index { it.include?('102') }
assert_equal(header2, header1 + 1)
assert((layout == 'reverse') == header_first ? input > header1 : input < header1)
# Footer
footer1 = lines.index { it.include?('201') }
footer2 = lines.index { it.include?('202') }
assert_equal(footer2, footer1 + 1)
assert(layout == 'reverse' ? footer1 > item2 : footer1 < item2)
# Header lines
hline1 = lines.index { it.include?('1001') }
hline2 = lines.index { it.include?('1002') }
assert_equal(hline1, layout == 'default' ? hline2 + 1 : hline2 - 1)
assert(layout == 'reverse' ? hline1 > header1 : hline1 < header1)
end
tmux.send_keys :Enter
end
end
end end

View File

@@ -189,6 +189,20 @@ class TestPreview < TestInteractive
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' } tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
end end
def test_preview_asterisk
tmux.send_keys %(seq 5 | #{FZF} --multi --preview 'echo [{}/{+}/{*}/{*n}]' --preview-window '+{1}'), :Enter
tmux.until { |lines| assert_equal 5, lines.match_count }
tmux.until { |lines| assert_includes lines[1], ' [1/1/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' [2/1/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys :BTab
tmux.until { |lines| assert_includes lines[1], ' [3/1 2/1 2 3 4 5/0 1 2 3 4] ' }
tmux.send_keys '5'
tmux.until { |lines| assert_includes lines[1], ' [5/1 2/5/4] ' }
tmux.send_keys '5'
tmux.until { |lines| assert_includes lines[1], ' [/1 2//] ' }
end
def test_preview_file def test_preview_file
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' } tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }

113
test/test_raw.rb Normal file
View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
require_relative 'lib/common'
# Testing raw mode
class TestRaw < TestInteractive
def test_raw_mode
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind ctrl-x:toggle-raw,a:enable-raw,b:disable-raw --gutter '▌' --multi --bind 'space:transform-prompt:echo "[[$FZF_RAW]] "'), :Enter
tmux.until { assert_equal 1000, it.match_count }
tmux.send_keys 1
tmux.until { assert_equal 272, it.match_count }
tmux.send_keys :Up
tmux.until { assert_includes it, '> 2' }
tmux.send_keys 'C-p'
tmux.until do
assert_includes it, '> 10'
assert_includes it, '▖ 9'
end
tmux.send_keys 'C-x'
tmux.until do
assert_includes it, '> 10'
assert_includes it, '▌ 1'
end
tmux.send_keys :Up, 'C-x'
tmux.until do
assert_includes it, '> 11'
assert_includes it, '▖ 10'
end
tmux.send_keys 1
tmux.until { assert_equal 28, it.match_count }
tmux.send_keys 'C-p'
tmux.until do
assert_includes it, '> 101'
assert_includes it, '▖ 100'
end
tmux.send_keys 'C-n'
tmux.until do
assert_includes it, '> 11'
assert_includes it, '▖ 10'
end
tmux.send_keys :Tab, :Tab, :Tab
tmux.until { assert_equal 3, it.select_count }
tmux.send_keys 'C-x'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '▌ 110'
assert_includes it, '>>11'
end
tmux.send_keys 'a'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '>>11'
assert_includes it, '▖ 10'
end
tmux.send_keys :Down, :Space
tmux.until { assert_includes it, '[[0]] 11' }
tmux.send_keys :Up, :Space
tmux.until { assert_includes it, '[[1]] 11' }
tmux.send_keys 'b'
tmux.until do
assert_equal 1, it.select_count
assert_includes it, '▌ 110'
assert_includes it, '>>11'
end
tmux.send_keys :Space
tmux.until { assert_includes it, '[[]] 11' }
tmux.send_keys 'C-u', '5'
tmux.until { assert_includes it, '> 5' }
tmux.send_keys 'C-x', 'C-p', 'C-p'
tmux.until do
assert_includes it, '> 25'
assert_includes it, '▖ 24'
end
tmux.send_keys 'C-x'
tmux.until do
assert_includes it, '> 25'
assert_includes it, '▌ 15'
end
# 35 is the closest match in raw mode
tmux.send_keys 'C-x', :Up, :Up, :Up, :Up, :Up, :Up, 'C-x'
tmux.until do
assert_includes it, '> 35'
assert_includes it, '▌ 25'
end
end
def test_raw_best
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind space:best), :Enter
tmux.send_keys 999
tmux.until { assert_includes it, '> 1' }
tmux.send_keys :Space
tmux.until { assert_includes it, '> 999' }
end
end

View File

@@ -64,7 +64,7 @@ remove_line() {
line_no=1 line_no=1
continue continue
fi fi
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 )) line_no=$(($(sed 's/:.*//' <<< "$line") + line_no - 1))
content=$(sed 's/^[0-9]*://' <<< "$line") content=$(sed 's/^[0-9]*://' <<< "$line")
match=1 match=1
echo " - Line #$line_no: $content" echo " - Line #$line_no: $content"
@@ -76,7 +76,7 @@ remove_line() {
echo " - Removed" echo " - Removed"
else else
echo " - Skipped" echo " - Skipped"
line_no=$(( line_no + 1 )) line_no=$((line_no + 1))
fi fi
done done
[ $match -eq 0 ] && echo " - Nothing found" [ $match -eq 0 ] && echo " - Nothing found"
@@ -109,6 +109,6 @@ if [ -d "${fish_dir}/functions" ]; then
fi fi
config_dir=$(dirname "$prefix_expand") config_dir=$(dirname "$prefix_expand")
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then
rmdir "$config_dir" rmdir "$config_dir"
fi fi