Compare commits

...

26 Commits

Author SHA1 Message Date
Junegunn Choi
b473477c22 Update README
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-12-04 08:36:49 +09:00
LangLangBart
fcc4178bca Updates man page documentation (#4607)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
* docs(man): Add example for {+f} in preview command
* docs(man): replace echo -e with printf to improve portability across different shells
2025-12-02 18:29:46 +09:00
LangLangBart
cfc37caabc feat(zsh): Handle multi-line history selection (#4595)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-30 21:32:55 +09:00
Junegunn Choi
af2a81dc02 Add Goods code
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
2025-11-29 14:39:43 +09:00
Junegunn Choi
be5a687281 Goods 2025-11-29 13:59:43 +09:00
RT
771e35b972 feat: add alt-gutter color option (#4602)
* Add alt-gutter color option

* Simplify the code

---------

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-11-29 10:43:13 +09:00
Junegunn Choi
60a5be1e65 Do not allow very long queries in FuzzyMatchV2
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Close #4608
2025-11-28 18:41:45 +09:00
junegunn
1d5e87f5e4 Deploying to master from @ junegunn/fzf@3db63f5e52 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-23 00:02:24 +00:00
LangLangBart
3db63f5e52 fix(terminal): correct display width calculation with maxWidth (#4596)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
fix #4593

* test(core): add test for --freeze-right with long ellipsis
2025-11-20 09:09:36 +09:00
Junegunn Choi
2ab923f3ae 0.67.0
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-16 20:02:39 +09:00
Massimo Mund
c3e6d9a8f9 Distinguish between Ctrl-H and Ctrl-Backspace in Windows (#4590)
Since you can actually distinguish between Ctrl-H and Ctrl-Backspace in Windows we need to reintroduce the tui.CtrlH constant. On *nix systems we map all Ctrl(-Alt)-h to Ctrl(-Alt)-Backspace internally, but you can use either in --bind.
2025-11-16 20:00:24 +09:00
Junegunn Choi
2471edf3ff Make ctrl-alt-h a synonym of ctrl-alt-backspace on non-Windows environment (#4589) 2025-11-16 16:33:53 +09:00
junegunn
53a8aeeb72 Deploying to master from @ junegunn/fzf@60b35e748b 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-16 00:02:13 +00:00
Junegunn Choi
60b35e748b Header and footer should not be wider than the list
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Example:
  WIDE=$(printf 'x%.0s' {1..1000})
  (echo $WIDE; echo $WIDE) |
    fzf --header-lines 1 --style full --ellipsis XX --header "$WIDE" \
        --no-header-lines-border --footer "$WIDE" --no-footer-border
2025-11-15 11:41:51 +09:00
Junegunn Choi
3f499f055e Avoid truncating ellipsis to avoid confusion
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-13 23:00:32 +09:00
Junegunn Choi
1df99db0b2 Keep the previous delimiter before frozen columns 2025-11-13 22:38:49 +09:00
dependabot[bot]
535b610a6b Bump github/codeql-action from 3 to 4 (#4554)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 10:48:43 +09:00
phanium
91fab3b3c2 Fix lint warnings (#4586)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
2025-11-12 22:05:17 +09:00
Junegunn Choi
b9f2bf64ff Add --freeze-right=N option to keep the rightmost N fields visible 2025-11-12 22:00:27 +09:00
Junegunn Choi
07d53cb7e4 Add --freeze-left=N option to keep the leftmost N fields visible 2025-11-12 22:00:27 +09:00
Massimo Mund
ead534a1be Fix modifier detection for Backspace / Ctrl-H on Windows (#4582)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Windows sends different key events and modifier combinations to theFullscreenRenderer than a tcell FullscreenRenderer on Linux (-tags tcell).
This led to Ctrl+H being misinterpreted (and therefore unbindable) on some Windows builds.

Basically reverts changes to `src/tui/tcell.go` introduced by `a0cabe0`.
2025-11-10 19:12:01 +09:00
Junegunn Choi
8a05083503 Fix reading an extra key after a terminal action
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Fix #4578
2025-11-09 15:36:07 +09:00
phanium
e659b46ff5 feat: append spinner in the end when --info=inline (#4567)
Test:
  go run main.go --query "$(seq 100)" --info inline --border < <(sleep 60)
  go run main.go --query "$(seq 100)" --info inline --info-command 'echo hello' --border < <(sleep 60)

Close #4344
Close #619

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2025-11-09 10:44:27 +09:00
Junegunn Choi
991c36453c [man] Add --gutter-raw
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Close #4579
2025-11-08 17:50:25 +09:00
junegunn
4d563c6dfa Deploying to master from @ junegunn/fzf@5cb695744f 🚀
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
2025-11-02 00:02:16 +00:00
Koichi Murase
5cb695744f [bash,zsh] Fix the version check for mawk (#4574)
Some checks failed
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
We have been checking the mawk version by extracting <x>, <y>, <z>,
and <d> part from "mawk <x>.<y>.<z> <d>" in the output of the "mawk -W
version" and testing <x>, <y>, <z>, and <d> using an arithmetic
evalaution.  However, <d> is ensured to be an integer only in "x.y.z
>= 1.3.4".  Otherwise, it may cause a syntax error in the arithmetic
evaluation.  The mawk started to include the date as an integer in the
<d> position only from mawk-1.3.3-20090721.  We should first check
that "x.y.z >= 1.3.4" and then check the value of "d".  In case, "mawk
-W version" produces a completely different text, we should also
redirect stderr of the arithmetic commands to /dev/null.
2025-10-31 21:14:41 +09:00
35 changed files with 622 additions and 211 deletions

View File

@@ -33,12 +33,12 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@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

@@ -22,6 +22,7 @@ builds:
- loong64 - loong64
- ppc64le - ppc64le
- s390x - s390x
- riscv64
goarm: goarm:
- "5" - "5"
- "6" - "6"
@@ -39,6 +40,8 @@ builds:
goarch: arm64 goarch: arm64
- goos: openbsd - goos: openbsd
goarch: arm64 goarch: arm64
- goos: openbsd
goarch: riscv64
- goos: android - goos: android
goarch: amd64 goarch: amd64
- goos: android - goos: android

View File

@@ -1,6 +1,32 @@
CHANGELOG CHANGELOG
========= =========
0.67.0
------
- Added `--freeze-left=N` option to keep the leftmost N columns always visible.
```sh
# Keep the file name column fixed and always visible
git grep --line-number --color=always -- '' |
fzf --ansi --delimiter : --freeze-left 1
# Can be used with --keep-right
git grep --line-number --color=always -- '' |
fzf --ansi --delimiter : --freeze-left 1 --keep-right
```
- Also added `--freeze-right=N` option to keep the rightmost N columns always visible.
```sh
# Stronger version of --keep-right that always keeps the right-end visible
fd | fzf --freeze-right 1
# Keep the base name always visible
fd | fzf --freeze-right 1 --delimiter /
# Keep both leftmost and rightmost components visible
fd | fzf --freeze-left 1 --freeze-right 1 --delimiter /
```
- Updated `--info=inline` to print the spinner (load indicator).
- Bug fixes
0.66.1 0.66.1
------ ------
- Bug fixes - Bug fixes

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.66.1 version=0.67.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
@@ -177,6 +177,7 @@ case "$archi" in
Linux\ aarch64\ Android) download fzf-$version-android_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\ riscv64*) download fzf-$version-linux_riscv64.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 ;;

View File

@@ -1,4 +1,4 @@
$version="0.66.1" $version="0.67.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.66" var version = "0.67"
var revision = "devel" var revision = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf\-tmux 1 "Oct 2025" "fzf 0.66.1" "fzf\-tmux - open fzf in tmux split pane" .TH fzf\-tmux 1 "Nov 2025" "fzf 0.67.0" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf\-tmux - open fzf in tmux split pane fzf\-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Oct 2025" "fzf 0.66.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Nov 2025" "fzf 0.67.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -272,6 +272,7 @@ color mappings. Each entry is separated by a comma and/or whitespaces.
\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 \fBalt\-bg \fRAlternate background color to create striped lines
\fBalt\-gutter \fRAlternate gutter color to create the striped pattern
\fBquery (input\-fg) \fRQuery string \fBquery (input\-fg) \fRQuery string
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default) \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)
@@ -629,9 +630,16 @@ Render empty lines between each item
The given string will be repeated to draw a horizontal line on each gap The given string will be repeated to draw a horizontal line on each gap
(default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR). (default: '┈' or '\-' depending on \fB\-\-no\-unicode\fR).
.TP .TP
.BI "\-\-freeze\-left=" "N"
Number of fields to freeze on the left.
.TP
.BI "\-\-freeze\-right=" "N"
Number of fields to freeze on the right.
.TP
.B "\-\-keep\-right" .B "\-\-keep\-right"
Keep the right end of the line visible when it's too long. Effective only when Keep the right end of the line visible when it's too long. Effective only when
the query string is empty. the query string is empty. Use \fB\-\-freeze\-right=1\fR instead if you want
the last field to be always visible even with a non-empty query.
.TP .TP
.BI "\-\-scroll\-off=" "LINES" .BI "\-\-scroll\-off=" "LINES"
Number of screen lines to keep above or below when scrolling to the top or to Number of screen lines to keep above or below when scrolling to the top or to
@@ -651,6 +659,9 @@ Label characters for \fBjump\fR mode.
.BI "\-\-gutter=" "CHAR" .BI "\-\-gutter=" "CHAR"
Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given) Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given)
.TP .TP
.BI "\-\-gutter\-raw=" "CHAR"
Character used for the gutter column in raw mode (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
@@ -730,7 +741,7 @@ ENVIRONMENT VARIABLES EXPORTED TO CHILD PROCESSES.
e.g. e.g.
\fB# Prepend the current cursor position in yellow \fB# Prepend the current cursor position in yellow
fzf \-\-info\-command='echo \-e "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR fzf \-\-info\-command='printf "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
.TP .TP
.B "\-\-no\-info" .B "\-\-no\-info"
@@ -830,6 +841,9 @@ e.g.
# 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 \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
\fB# Use {+f} to get the selected items as a line-separated list
seq 100 | fzf \-\-multi \-\-bind 'enter:become:cat {+f}'\fR
Also, Also,
* \fB{q}\fR is replaced to the current query string * \fB{q}\fR is replaced to the current query string
@@ -1485,7 +1499,7 @@ e.g.
.br .br
\fIctrl\-/\fR (\fIctrl\-_\fR) \fIctrl\-/\fR (\fIctrl\-_\fR)
.br .br
\fIctrl\-alt\-[a\-z]\fR \fIctrl\-alt\-[a\-z]\fR (\fIctrl\-alt\-h\fR is \fIctrl\-alt\-backspace\fR on non-Windows)
.br .br
\fIalt\-[*]\fR (Any case-sensitive single character is allowed) \fIalt\-[*]\fR (Any case-sensitive single character is allowed)
.br .br
@@ -1615,7 +1629,7 @@ e.g.
.br .br
\fIctrl\-alt\-end\fR \fIctrl\-alt\-end\fR
.br .br
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR) \fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR) (\fIctrl\-alt\-h\fR (non-Windows))
.br .br
\fIctrl\-alt\-delete\fR \fIctrl\-alt\-delete\fR
.br .br
@@ -2095,7 +2109,7 @@ payload of HTTP POST request to the \fB\-\-listen\fR server.
e.g. e.g.
\fB# Disallow selecting an empty line \fB# Disallow selecting an empty line
echo \-e "1. Hello\\n2. Goodbye\\n\\n3. Exit" | printf "1. Hello\\n2. Goodbye\\n\\n3. Exit" |
fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\ fzf \-\-height '~100%' \-\-reverse \-\-header 'Select one' \\
\-\-bind 'enter:transform:[[ \-n {} ]] && \-\-bind 'enter:transform:[[ \-n {} ]] &&
echo accept || echo accept ||

View File

@@ -26,7 +26,10 @@ __fzf_exec_awk() {
# version >= 1.3.4 # version >= 1.3.4
local n x y z d local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null) 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 [[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi fi
fi fi
# Note: macOS awk has a quirk that it stops processing at all when it sees # Note: macOS awk has a quirk that it stops processing at all when it sees

View File

@@ -51,7 +51,10 @@ __fzf_exec_awk() {
elif command -v mawk > /dev/null 2>&1; then elif command -v mawk > /dev/null 2>&1; then
local n x y z d local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null) 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 [[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi fi
fi fi
LC_ALL=C exec "$__fzf_awk" "$@" LC_ALL=C exec "$__fzf_awk" "$@"

View File

@@ -115,7 +115,10 @@ __fzf_exec_awk() {
elif command -v mawk > /dev/null 2>&1; then elif command -v mawk > /dev/null 2>&1; then
local n x y z d local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null) 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 [[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi fi
fi fi
LC_ALL=C exec "$__fzf_awk" "$@" LC_ALL=C exec "$__fzf_awk" "$@"

View File

@@ -38,7 +38,10 @@ __fzf_exec_awk() {
elif command -v mawk > /dev/null 2>&1; then elif command -v mawk > /dev/null 2>&1; then
local n x y z d local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null) 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 [[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi fi
fi fi
LC_ALL=C exec "$__fzf_awk" "$@" LC_ALL=C exec "$__fzf_awk" "$@"

View File

@@ -58,7 +58,10 @@ __fzf_exec_awk() {
elif command -v mawk > /dev/null 2>&1; then elif command -v mawk > /dev/null 2>&1; then
local n x y z d local n x y z d
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null) 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 [[ $n == mawk ]] &&
(((x * 1000 + y) * 1000 + z >= 1003004)) 2> /dev/null &&
((d >= 20230302)) 2> /dev/null &&
__fzf_awk=mawk
fi fi
fi fi
LC_ALL=C exec "$__fzf_awk" "$@" LC_ALL=C exec "$__fzf_awk" "$@"
@@ -125,25 +128,52 @@ 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
fzf-history-widget() { fzf-history-widget() {
local selected local selected extracted_with_perl=0
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases no_glob no_ksharrays extendedglob 2> /dev/null
# Ensure the module is loaded if not already, and the required features, such # Ensure the module is loaded if not already, and the required features, such
# as the associative 'history' array, which maps event numbers to full history # as the associative 'history' array, which maps event numbers to full history
# lines, are set. Also, make sure Perl is installed for multi-line output. # lines, are set. Also, make sure Perl is installed for multi-line output.
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,alt-r:toggle-raw --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 --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
extracted_with_perl=1
else else
selected="$(fc -rl 1 | __fzf_exec_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,alt-r:toggle-raw --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 --multi ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER}") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))" FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
fi fi
local ret=$? local ret=$?
local -a cmds
# Avoid leaking auto assigned values when using backreferences '(#b)'
local -a mbegin mend match
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then # Heuristic to check if the selected value is from history or a custom query
zle vi-fetch-history -n $MATCH if ((( extracted_with_perl )) && [[ $selected == <->$'\t'* ]]) ||
((( ! extracted_with_perl )) && [[ $selected == [[:blank:]]#<->( |\* )* ]]); then
# Split at newlines
for line in ${(ps:\n:)selected}; do
if (( extracted_with_perl )); then
if [[ $line == (#b)(<->)(#B)$'\t'* ]]; then
(( ${+history[${match[1]}]} )) && cmds+=("${history[${match[1]}]}")
fi
elif [[ $line == [[:blank:]]#(#b)(<->)(#B)( |\* )* ]]; then
# Avoid $history array: lags behind 'fc' on foreign commands (*)
# https://zsh.org/mla/users/2024/msg00692.html
# Push BUFFER onto stack; fetch and save history entry from BUFFER; restore
zle .push-line
zle vi-fetch-history -n ${match[1]}
(( ${#BUFFER} )) && cmds+=("${BUFFER}")
BUFFER=""
zle .get-line
fi
done
if (( ${#cmds[@]} )); then
# Join by newline after stripping trailing newlines from each command
BUFFER="${(pj:\n:)${(@)cmds%%$'\n'#}}"
CURSOR=${#BUFFER}
fi
else # selected is a custom query, not from history else # selected is a custom query, not from history
LBUFFER="$selected" LBUFFER="$selected"
fi fi

View File

@@ -365,7 +365,7 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int
firstIdx, idx, lastIdx := 0, 0, 0 firstIdx, idx, lastIdx := 0, 0, 0
var b byte var b byte
for pidx := 0; pidx < len(pattern); pidx++ { for pidx := range pattern {
b = byte(pattern[pidx]) b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx) idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 { if idx < 0 {
@@ -445,7 +445,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
// Since O(nm) algorithm can be prohibitively expensive for large input, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
if slab != nil && N*M > cap(slab.I16) { // Also, we should not allow a very long pattern to avoid 16-bit integer
// overflow in the score matrix. 1000 is a safe limit.
if slab != nil && N*M > cap(slab.I16) || M > 1000 {
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
} }
@@ -726,7 +728,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
lenRunes := text.Length() lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
for index := 0; index < lenRunes; index++ { for index := range lenRunes {
char := text.Get(indexAt(index, lenRunes, forward)) char := text.Get(indexAt(index, lenRunes, forward))
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string

View File

@@ -41,7 +41,7 @@ func testParserReference(t testing.TB, str string) {
equal := len(got) == len(exp) equal := len(got) == len(exp)
if equal { if equal {
for i := 0; i < len(got); i++ { for i := range got {
if got[i] != exp[i] { if got[i] != exp[i] {
equal = false equal = false
break break
@@ -167,9 +167,9 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
randomString := func(rr *rand.Rand) string { randomString := func(rr *rand.Rand) string {
numChars := rand.Intn(50) numChars := rand.Intn(50)
codePoints := make([]rune, numChars) codePoints := make([]rune, numChars)
for i := 0; i < len(codePoints); i++ { for i := range codePoints {
var r rune var r rune
for n := 0; n < 1000; n++ { for range 1000 {
r = rune(rr.Intn(utf8.MaxRune)) r = rune(rr.Intn(utf8.MaxRune))
// Allow 10% of runes to be invalid // Allow 10% of runes to be invalid
if utf8.ValidRune(r) || rr.Float64() < 0.10 { if utf8.ValidRune(r) || rr.Float64() < 0.10 {
@@ -182,7 +182,7 @@ func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
} }
rr := rand.New(rand.NewSource(1)) rr := rand.New(rand.NewSource(1))
for i := 0; i < 100_000; i++ { for range 100_000 {
testParserReference(t, randomString(rr)) testParserReference(t, randomString(rr))
} }
} }

View File

@@ -51,7 +51,7 @@ func TestChunkList(t *testing.T) {
} }
// Add more data // Add more data
for i := 0; i < chunkSize*2; i++ { for i := range chunkSize * 2 {
cl.Push(fmt.Appendf(nil, "item %d", i)) cl.Push(fmt.Appendf(nil, "item %d", i))
} }
@@ -85,7 +85,7 @@ func TestChunkListTail(t *testing.T) {
return true return true
}) })
total := chunkSize*2 + chunkSize/2 total := chunkSize*2 + chunkSize/2
for i := 0; i < total; i++ { for i := range total {
cl.Push(fmt.Appendf(nil, "item %d", i)) cl.Push(fmt.Appendf(nil, "item %d", i))
} }

View File

@@ -502,7 +502,7 @@ func Run(opts *Options) (int, error) {
return item.acceptNth(opts.Ansi, opts.Delimiter, fn) return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
} }
} }
for i := 0; i < count; i++ { for i := range count {
opts.Printer(transformer(merger.Get(i).item)) opts.Printer(transformer(merger.Get(i).item))
} }
if count == 0 { if count == 0 {

View File

@@ -38,7 +38,7 @@ func TestHistory(t *testing.T) {
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }
for i := 0; i < maxHistory; i++ { for i := range maxHistory {
if h.lines[i] != "foobar" { if h.lines[i] != "foobar" {
t.Error("Expected: foobar, actual: " + h.lines[i]) t.Error("Expected: foobar, actual: " + h.lines[i])
} }

View File

@@ -34,11 +34,11 @@ func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4 numLists := 4
lists := make([][]Result, numLists) lists := make([][]Result, numLists)
cnt := 0 cnt := 0
for i := 0; i < numLists; i++ { for i := range numLists {
numResults := rand.Int() % 20 numResults := rand.Int() % 20
cnt += numResults cnt += numResults
lists[i] = make([]Result, numResults) lists[i] = make([]Result, numResults)
for j := 0; j < numResults; j++ { for j := range numResults {
item := randResult() item := randResult()
lists[i][j] = item lists[i][j] = item
} }
@@ -60,7 +60,7 @@ func TestMergerUnsorted(t *testing.T) {
// Not sorted: same order // Not sorted: same order
mg := NewMerger(nil, lists, false, false, revision{}, 0, 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 := range cnt {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
} }
} }
@@ -73,7 +73,7 @@ func TestMergerSorted(t *testing.T) {
mg := NewMerger(nil, lists, true, false, revision{}, 0, 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 := range cnt {
if items[i] != mg.Get(i) { if items[i] != mg.Get(i) {
t.Error("Not sorted", items[i], mg.Get(i)) t.Error("Not sorted", items[i], mg.Get(i))
} }

View File

@@ -104,6 +104,8 @@ Usage: fzf [options]
--gap[=N] Render empty lines between each item --gap[=N] Render empty lines between each item
--gap-line[=STR] Draw horizontal line on each gap using the string --gap-line[=STR] Draw horizontal line on each gap using the string
(default: '┈' or '-') (default: '┈' or '-')
--freeze-left=N Number of fields to freeze on the left
--freeze-right=N Number of fields to freeze on the right
--keep-right Keep the right end of the line visible on overflow --keep-right Keep the right end of the line visible on overflow
--scroll-off=LINES Number of screen lines to keep above or below when --scroll-off=LINES Number of screen lines to keep above or below when
scrolling to the top or to the bottom (default: 0) scrolling to the top or to the bottom (default: 0)
@@ -562,6 +564,8 @@ type Options struct {
Case Case Case Case
Normalize bool Normalize bool
Nth []Range Nth []Range
FreezeLeft int
FreezeRight int
WithNth func(Delimiter) func([]Token, int32) string WithNth func(Delimiter) func([]Token, int32) string
AcceptNth func(Delimiter) func([]Token, int32) string AcceptNth func(Delimiter) func([]Token, int32) string
Delimiter Delimiter Delimiter Delimiter
@@ -1213,11 +1217,20 @@ func parseKeyChords(str string, message string) (map[tui.Event]string, []tui.Eve
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]) {
evt := tui.CtrlAltKey(rune(key[9])) r := rune(lkey[9])
evt := tui.CtrlAltKey(r)
if r == 'h' && !util.IsWindows() {
evt = tui.CtrlAltBackspace.AsEvent()
}
chords[evt] = key chords[evt] = key
list = append(list, evt) 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')) evt := tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')
r := rune(lkey[5])
if r == 'h' && !util.IsWindows() {
evt = tui.CtrlBackspace
}
add(evt)
} else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") { } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
r := runes[4] r := runes[4]
switch r { switch r {
@@ -1463,6 +1476,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) (*tui.ColorTheme, *tui
mergeAttr(&theme.Nomatch) mergeAttr(&theme.Nomatch)
case "gutter": case "gutter":
mergeAttr(&theme.Gutter) mergeAttr(&theme.Gutter)
case "alt-gutter":
mergeAttr(&theme.AltGutter)
case "hl": case "hl":
mergeAttr(&theme.Match) mergeAttr(&theme.Match)
case "current-hl", "hl+": case "current-hl", "hl+":
@@ -2695,6 +2710,14 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.Nth, err = splitNth(str); err != nil { if opts.Nth, err = splitNth(str); err != nil {
return err return err
} }
case "--freeze-left":
if opts.FreezeLeft, err = nextInt("number of fields required"); err != nil {
return err
}
case "--freeze-right":
if opts.FreezeRight, err = nextInt("number of fields required"); err != nil {
return err
}
case "--with-nth": case "--with-nth":
str, err := nextString("nth expression required") str, err := nextString("nth expression required")
if err != nil { if err != nil {
@@ -3338,6 +3361,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
return errors.New("empty jump labels") return errors.New("empty jump labels")
} }
if opts.FreezeLeft < 0 || opts.FreezeRight < 0 {
return errors.New("number of fields to freeze must be a non-negative integer")
}
if validateJumpLabels { if validateJumpLabels {
for _, r := range opts.JumpLabels { for _, r := range opts.JumpLabels {
if r < 32 || r > 126 { if r < 32 || r > 126 {

View File

@@ -7,6 +7,7 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -178,7 +179,7 @@ func (r *Reader) feed(src io.Reader) {
for { for {
n := 0 n := 0
scope := slab[:util.Min(len(slab), readerBufferSize)] scope := slab[:util.Min(len(slab), readerBufferSize)]
for i := 0; i < 100; i++ { for range 100 {
n, err = src.Read(scope) n, err = src.Read(scope)
if n > 0 || err != nil { if n > 0 || err != nil {
break break
@@ -308,15 +309,11 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
if !opts.hidden && base[0] == '.' && base != ".." { if !opts.hidden && base[0] == '.' && base != ".." {
return filepath.SkipDir return filepath.SkipDir
} }
for _, ignore := range ignoresBase { if slices.Contains(ignoresBase, base) {
if ignore == base { return filepath.SkipDir
return filepath.SkipDir
}
} }
for _, ignore := range ignoresFull { if slices.Contains(ignoresFull, path) {
if ignore == path { return filepath.SkipDir
return filepath.SkipDir
}
} }
for _, ignore := range ignoresSuffix { for _, ignore := range ignoresSuffix {
if strings.HasSuffix(path, ignore) { if strings.HasSuffix(path, ignore) {

View File

@@ -91,7 +91,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
case byBegin, byEnd: case byBegin, byEnd:
if validOffsetFound { if validOffsetFound {
whitePrefixLen := 0 whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ { for idx := range numChars {
r := item.text.Get(idx) r := item.text.Get(idx)
whitePrefixLen = idx whitePrefixLen = idx
if idx == minBegin || !unicode.IsSpace(r) { if idx == minBegin || !unicode.IsSpace(r) {

View File

@@ -331,6 +331,8 @@ type Terminal struct {
scrollbar string scrollbar string
previewScrollbar string previewScrollbar string
ansi bool ansi bool
freezeLeft int
freezeRight int
nthAttr tui.Attr nthAttr tui.Attr
nth []Range nth []Range
nthCurrent []Range nthCurrent []Range
@@ -496,6 +498,14 @@ const (
reqFatal reqFatal
) )
func isTerminalEvent(et util.EventType) bool {
switch et {
case reqClose, reqPrintQuery, reqBecome, reqQuit, reqFatal:
return true
}
return false
}
type action struct { type action struct {
t actionType t actionType
a string a string
@@ -1050,6 +1060,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
footer: opts.Footer, footer: opts.Footer,
header0: opts.Header, header0: opts.Header,
ansi: opts.Ansi, ansi: opts.Ansi,
freezeLeft: opts.FreezeLeft,
freezeRight: opts.FreezeRight,
nthAttr: opts.Theme.Nth.Attr, nthAttr: opts.Theme.Nth.Attr,
nth: opts.Nth, nth: opts.Nth,
nthCurrent: opts.Nth, nthCurrent: opts.Nth,
@@ -2478,6 +2490,8 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if shape.HasRight() { if shape.HasRight() {
width++ width++
} }
// Make sure that the width does not exceed the list width
width = util.Min(t.window.Width()+t.headerIndentImpl(0, shape), width)
height := b.Height() - borderLines(shape) height := b.Height() - borderLines(shape)
return t.tui.NewWindow(top, left, width, height, windowType, noBorder, true) return t.tui.NewWindow(top, left, width, height, windowType, noBorder, true)
} }
@@ -2981,6 +2995,11 @@ func (t *Terminal) printInfoImpl() {
} else { } else {
outputPrinter(t.window, maxWidth) outputPrinter(t.window, maxWidth)
} }
if t.infoStyle == infoInline && outputLen < maxWidth-1 && t.reading {
t.window.Print(" ")
printSpinner()
outputLen += 2
}
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 { if t.separatorLen > 0 {
@@ -3090,7 +3109,11 @@ func (t *Terminal) printFooter() {
} }
func (t *Terminal) headerIndent(borderShape tui.BorderShape) int { func (t *Terminal) headerIndent(borderShape tui.BorderShape) int {
indentSize := t.pointerLen + t.markerLen return t.headerIndentImpl(t.pointerLen+t.markerLen, borderShape)
}
func (t *Terminal) headerIndentImpl(base int, borderShape tui.BorderShape) int {
indentSize := base
if t.listBorderShape.HasLeft() { if t.listBorderShape.HasLeft() {
indentSize += 1 + t.borderWidth indentSize += 1 + t.borderWidth
} }
@@ -3179,14 +3202,22 @@ func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
t.renderBar(line, barRange) t.renderBar(line, barRange)
} }
func (t *Terminal) gutter(current bool) { func (t *Terminal) gutter(current bool, alt bool) {
var color tui.ColorPair var color tui.ColorPair
if current { if current {
color = tui.ColCurrentCursorEmpty color = tui.ColCurrentCursorEmpty
} else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse { } else if !t.raw && t.gutterReverse || t.raw && t.gutterRawReverse {
color = tui.ColCursorEmpty if alt {
color = tui.ColAltCursorEmpty
} else {
color = tui.ColCursorEmpty
}
} else { } else {
color = tui.ColCursorEmptyChar if alt {
color = tui.ColAltCursorEmptyChar
} else {
color = tui.ColCursorEmptyChar
}
} }
gutter := t.pointerEmpty gutter := t.pointerEmpty
if t.raw { if t.raw {
@@ -3197,7 +3228,7 @@ func (t *Terminal) gutter(current bool) {
func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) { func (t *Terminal) renderGapLine(line int, barRange [2]int, drawLine bool) {
t.move(line, 0, false) t.move(line, 0, false)
t.gutter(false) t.gutter(false, false)
t.window.Print(t.markerEmpty) t.window.Print(t.markerEmpty)
x := t.pointerLen + t.markerLen x := t.pointerLen + t.markerLen
@@ -3371,7 +3402,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
return indentSize return indentSize
} }
if len(label) == 0 { if len(label) == 0 {
t.gutter(true) t.gutter(true, false)
} else { } else {
t.window.CPrint(tui.ColCurrentCursor, label) t.window.CPrint(tui.ColCurrentCursor, label)
} }
@@ -3393,7 +3424,7 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
return indentSize return indentSize
} }
if len(label) == 0 { if len(label) == 0 {
t.gutter(false) t.gutter(false, index%2 == 1)
} else { } else {
t.window.CPrint(tui.ColCursor, label) t.window.CPrint(tui.ColCursor, label)
} }
@@ -3509,17 +3540,48 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
} else { } else {
tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent) tokens = Transform(Tokenize(item.text.ToString(), t.delimiter), t.nthCurrent)
} }
for _, token := range tokens { nthOffsets = make([]Offset, len(tokens))
for i, token := range tokens {
start := token.prefixLength start := token.prefixLength
length := token.text.Length() - token.text.TrailingWhitespaces() length := token.text.Length() - token.text.TrailingWhitespaces()
end := start + int32(length) end := start + int32(length)
nthOffsets = append(nthOffsets, Offset{int32(start), int32(end)}) nthOffsets[i] = Offset{int32(start), int32(end)}
} }
sort.Sort(ByOrder(nthOffsets)) sort.Sort(ByOrder(nthOffsets))
} }
} }
allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, hidden) allOffsets := result.colorOffsets(charOffsets, nthOffsets, t.theme, colBase, colMatch, t.nthAttr, hidden)
// Determine split offset for horizontal scrolling with freeze
splitOffset1 := -1
splitOffset2 := -1
if t.hscroll && !t.wrap {
var tokens []Token
if t.freezeLeft > 0 || t.freezeRight > 0 {
tokens = Tokenize(item.text.ToString(), t.delimiter)
}
// 0 | 1 | 2 | 3 | 4 | 5
// ------> <------
if t.freezeLeft > 0 {
if len(tokens) > 0 {
token := tokens[util.Min(t.freezeLeft, len(tokens))-1]
splitOffset1 = int(token.prefixLength) + token.text.Length() - token.text.TrailingWhitespaces()
}
}
if t.freezeRight > 0 {
index := util.Max(t.freezeLeft-1, len(tokens)-t.freezeRight-1)
if index < 0 {
splitOffset2 = 0
} else if index >= t.freezeLeft {
token := tokens[index]
delimiter := strings.TrimLeftFunc(GetLastDelimiter(token.text.ToString(), t.delimiter), unicode.IsSpace)
splitOffset2 = int(token.prefixLength) + token.text.Length() - len([]rune(delimiter))
}
splitOffset2 = util.Max(splitOffset2, splitOffset1)
}
}
maxLines := 1 maxLines := 1
if t.canSpanMultiLines() { if t.canSpanMultiLines() {
maxLines = maxLineNum - lineNum + 1 maxLines = maxLineNum - lineNum + 1
@@ -3589,16 +3651,24 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
break break
} }
} }
splitOffsetLeft := 0
if splitOffset1 >= 0 && splitOffset1 > from && splitOffset1 < from+len(line) {
splitOffsetLeft = splitOffset1 - from
}
splitOffsetRight := -1
if splitOffset2 >= 0 && splitOffset2 >= from && splitOffset2 < from+len(line) {
splitOffsetRight = splitOffset2 - from
}
from += len(line) from += len(line)
if lineOffset < skipLines { if lineOffset < skipLines {
continue continue
} }
actualLineOffset := lineOffset - skipLines actualLineOffset := lineOffset - skipLines
var maxe int var maxEnd int
for _, offset := range offsets { for _, offset := range offsets {
if offset.match { if offset.match {
maxe = util.Max(maxe, int(offset.offset[1])) maxEnd = util.Max(maxEnd, int(offset.offset[1]))
} }
} }
@@ -3662,69 +3732,117 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
wrapped = true wrapped = true
} }
displayWidth = t.displayWidthWithLimit(line, 0, maxWidth) frozenLeft := line[:splitOffsetLeft]
if !t.wrap && displayWidth > maxWidth { middle := line[splitOffsetLeft:]
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2) frozenRight := []rune{}
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line)) if splitOffsetRight >= splitOffsetLeft {
transformOffsets := func(diff int32, rightTrim bool) { middle = line[splitOffsetLeft:splitOffsetRight]
for idx, offset := range offsets { frozenRight = line[splitOffsetRight:]
b, e := offset.offset[0], offset.offset[1] }
el := int32(len(ellipsis)) displayWidthSum := 0
b += el - diff todo := [3]func(){}
e += el - diff for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} {
b = util.Max32(b, el) if len(runes) == 0 {
if rightTrim { continue
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
} }
if t.hscroll { shift := 0
if t.keepRight && pos == nil { maxe := maxEnd
trimmed, diff := t.trimLeft(line, maxWidth, ellipsisWidth) offs := make([]colorOffset, len(offsets))
transformOffsets(diff, false) for idx := range offsets {
line = append(ellipsis, trimmed...) offs[idx] = offsets[idx]
} else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) { if fidx == 1 && splitOffsetRight > 0 {
// Stri.. shift = splitOffsetRight
line, _ = t.trimRight(line, maxWidth-ellipsisWidth) } else if fidx == 2 && splitOffsetLeft > 0 {
line = append(line, ellipsis...) shift = splitOffsetLeft
} else { }
// Stri.. offs[idx].offset[0] -= int32(shift)
rightTrim := false offs[idx].offset[1] -= int32(shift)
if t.overflow(line[maxe:], ellipsisWidth) { }
line = append(line[:maxe], ellipsis...) maxe -= shift
rightTrim = true ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth)
adjustedMaxWidth := maxWidth
if fidx < 2 {
// For frozen parts, reserve space for the ellipsis in the middle part
adjustedMaxWidth -= ellipsisWidth
}
displayWidth = t.displayWidthWithLimit(runes, 0, adjustedMaxWidth)
if !t.wrap && displayWidth > adjustedMaxWidth {
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
transformOffsets := func(diff int32, rightTrim bool) {
for idx, offset := range offs {
b, e := offset.offset[0], offset.offset[1]
el := int32(len(ellipsis))
b += el - diff
e += el - diff
b = util.Max32(b, el)
if rightTrim {
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
}
offs[idx].offset[0] = b
offs[idx].offset[1] = util.Max32(b, e)
} }
// ..ri.. }
var diff int32 if t.hscroll {
line, diff = t.trimLeft(line, maxWidth, ellipsisWidth) if fidx == 1 || fidx == 2 && t.keepRight && pos == nil {
trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth)
transformOffsets(diff, false)
runes = append(ellipsis, trimmed...)
} else if fidx == 0 || !t.overflow(runes[:maxe], maxWidth-ellipsisWidth) {
// Stri..
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
runes = append(runes, ellipsis...)
} else {
// Stri..
rightTrim := false
if t.overflow(runes[maxe:], ellipsisWidth) {
runes = append(runes[:maxe], ellipsis...)
rightTrim = true
}
// ..ri..
var diff int32
runes, diff = t.trimLeft(runes, maxWidth, ellipsisWidth)
// Transform offsets // Transform offsets
transformOffsets(diff, rightTrim) transformOffsets(diff, rightTrim)
line = append(ellipsis, line...) runes = append(ellipsis, runes...)
}
} else {
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
runes = append(runes, ellipsis...)
for idx, offset := range offs {
offs[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offs[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
}
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
}
displayWidthSum += displayWidth
if maxWidth > 0 {
color := colBase
if hidden {
color = color.WithFg(t.theme.Nomatch)
}
todo[fidx] = func() {
t.printColoredString(t.window, runes, offs, color)
} }
} else { } else {
line, _ = t.trimRight(line, maxWidth-ellipsisWidth) break
line = append(line, ellipsis...)
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
} }
displayWidth = t.displayWidthWithLimit(line, 0, displayWidth) maxWidth -= displayWidth
} }
if todo[0] != nil {
if maxWidth > 0 { todo[0]()
color := colBase }
if hidden { if todo[2] != nil {
color = color.WithFg(t.theme.Nomatch) todo[2]()
} }
t.printColoredString(t.window, line, offsets, color) if todo[1] != nil {
todo[1]()
} }
if postTask != nil { if postTask != nil {
postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw, lbg) postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg)
} else { } else {
t.markOtherLine(actualLineNum) t.markOtherLine(actualLineNum)
} }
@@ -4806,7 +4924,7 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, [3][]*I
if asterisk { if asterisk {
cnt := t.merger.Length() cnt := t.merger.Length()
all = make([]*Item, cnt) all = make([]*Item, cnt)
for i := 0; i < cnt; i++ { for i := range cnt {
all[i] = t.merger.Get(i).item all[i] = t.merger.Get(i).item
} }
} }
@@ -5523,7 +5641,7 @@ func (t *Terminal) Loop() error {
req := func(evts ...util.EventType) { req := func(evts ...util.EventType) {
for _, event := range evts { for _, event := range evts {
events = append(events, event) events = append(events, event)
if event == reqClose || event == reqQuit { if isTerminalEvent(event) {
looping = false looping = false
} }
} }
@@ -7058,7 +7176,7 @@ func (t *Terminal) constrain() {
// May need to try again after adjusting the offset // May need to try again after adjusting the offset
t.offset = util.Constrain(t.offset, 0, count) t.offset = util.Constrain(t.offset, 0, count)
for tries := 0; tries < maxLines; tries++ { for range maxLines {
numItems := maxLines numItems := maxLines
// How many items can be fit on screen including the current item? // How many items can be fit on screen including the current item?
if t.canSpanMultiLines() && t.merger.Length() > 0 { if t.canSpanMultiLines() && t.merger.Length() > 0 {
@@ -7112,7 +7230,7 @@ func (t *Terminal) constrain() {
scrollOff := util.Min(maxLines/2, t.scrollOff) scrollOff := util.Min(maxLines/2, t.scrollOff)
newOffset := t.offset newOffset := t.offset
// 2-phase adjustment to avoid infinite loop of alternating between moving up and down // 2-phase adjustment to avoid infinite loop of alternating between moving up and down
for phase := 0; phase < 2; phase++ { for phase := range 2 {
for { for {
prevOffset := newOffset prevOffset := newOffset
numItems := t.merger.Length() numItems := t.merger.Length()

View File

@@ -206,8 +206,9 @@ func Tokenize(text string, delimiter Delimiter) []Token {
if delimiter.regex != nil { if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(text, -1) locs := delimiter.regex.FindAllStringIndex(text, -1)
begin := 0 begin := 0
for _, loc := range locs { tokens = make([]string, len(locs))
tokens = append(tokens, text[begin:loc[1]]) for i, loc := range locs {
tokens[i] = text[begin:loc[1]]
begin = loc[1] begin = loc[1]
} }
if begin < len(text) { if begin < len(text) {
@@ -233,6 +234,23 @@ func StripLastDelimiter(str string, delimiter Delimiter) string {
return strings.TrimRightFunc(str, unicode.IsSpace) return strings.TrimRightFunc(str, unicode.IsSpace)
} }
func GetLastDelimiter(str string, delimiter Delimiter) string {
if delimiter.str != nil {
if strings.HasSuffix(str, *delimiter.str) {
return *delimiter.str
}
} else if delimiter.regex != nil {
locs := delimiter.regex.FindAllStringIndex(str, -1)
if len(locs) > 0 {
lastLoc := locs[len(locs)-1]
if lastLoc[1] == len(str) {
return str[lastLoc[0]:]
}
}
}
return ""
}
// JoinTokens concatenates the tokens into a single string // JoinTokens concatenates the tokens into a single string
func JoinTokens(tokens []Token) string { func JoinTokens(tokens []Token) string {
var output bytes.Buffer var output bytes.Buffer

View File

@@ -16,7 +16,7 @@ func _() {
_ = x[CtrlE-5] _ = x[CtrlE-5]
_ = x[CtrlF-6] _ = x[CtrlF-6]
_ = x[CtrlG-7] _ = x[CtrlG-7]
_ = x[CtrlBackspace-8] _ = x[CtrlH-8]
_ = x[Tab-9] _ = x[Tab-9]
_ = x[CtrlJ-10] _ = x[CtrlJ-10]
_ = x[CtrlK-11] _ = x[CtrlK-11]
@@ -99,74 +99,75 @@ func _() {
_ = x[CtrlRight-88] _ = x[CtrlRight-88]
_ = x[CtrlHome-89] _ = x[CtrlHome-89]
_ = x[CtrlEnd-90] _ = x[CtrlEnd-90]
_ = x[CtrlDelete-91] _ = x[CtrlBackspace-91]
_ = x[CtrlPageUp-92] _ = x[CtrlDelete-92]
_ = x[CtrlPageDown-93] _ = x[CtrlPageUp-93]
_ = x[Alt-94] _ = x[CtrlPageDown-94]
_ = x[CtrlAlt-95] _ = x[Alt-95]
_ = x[CtrlAltUp-96] _ = x[CtrlAlt-96]
_ = x[CtrlAltDown-97] _ = x[CtrlAltUp-97]
_ = x[CtrlAltLeft-98] _ = x[CtrlAltDown-98]
_ = x[CtrlAltRight-99] _ = x[CtrlAltLeft-99]
_ = x[CtrlAltHome-100] _ = x[CtrlAltRight-100]
_ = x[CtrlAltEnd-101] _ = x[CtrlAltHome-101]
_ = x[CtrlAltBackspace-102] _ = x[CtrlAltEnd-102]
_ = x[CtrlAltDelete-103] _ = x[CtrlAltBackspace-103]
_ = x[CtrlAltPageUp-104] _ = x[CtrlAltDelete-104]
_ = x[CtrlAltPageDown-105] _ = x[CtrlAltPageUp-105]
_ = x[CtrlShiftUp-106] _ = x[CtrlAltPageDown-106]
_ = x[CtrlShiftDown-107] _ = x[CtrlShiftUp-107]
_ = x[CtrlShiftLeft-108] _ = x[CtrlShiftDown-108]
_ = x[CtrlShiftRight-109] _ = x[CtrlShiftLeft-109]
_ = x[CtrlShiftHome-110] _ = x[CtrlShiftRight-110]
_ = x[CtrlShiftEnd-111] _ = x[CtrlShiftHome-111]
_ = x[CtrlShiftDelete-112] _ = x[CtrlShiftEnd-112]
_ = x[CtrlShiftPageUp-113] _ = x[CtrlShiftDelete-113]
_ = x[CtrlShiftPageDown-114] _ = x[CtrlShiftPageUp-114]
_ = x[CtrlAltShiftUp-115] _ = x[CtrlShiftPageDown-115]
_ = x[CtrlAltShiftDown-116] _ = x[CtrlAltShiftUp-116]
_ = x[CtrlAltShiftLeft-117] _ = x[CtrlAltShiftDown-117]
_ = x[CtrlAltShiftRight-118] _ = x[CtrlAltShiftLeft-118]
_ = x[CtrlAltShiftHome-119] _ = x[CtrlAltShiftRight-119]
_ = x[CtrlAltShiftEnd-120] _ = x[CtrlAltShiftHome-120]
_ = x[CtrlAltShiftDelete-121] _ = x[CtrlAltShiftEnd-121]
_ = x[CtrlAltShiftPageUp-122] _ = x[CtrlAltShiftDelete-122]
_ = x[CtrlAltShiftPageDown-123] _ = x[CtrlAltShiftPageUp-123]
_ = x[Invalid-124] _ = x[CtrlAltShiftPageDown-124]
_ = x[Fatal-125] _ = x[Invalid-125]
_ = x[BracketedPasteBegin-126] _ = x[Fatal-126]
_ = x[BracketedPasteEnd-127] _ = x[BracketedPasteBegin-127]
_ = x[Mouse-128] _ = x[BracketedPasteEnd-128]
_ = x[DoubleClick-129] _ = x[Mouse-129]
_ = x[LeftClick-130] _ = x[DoubleClick-130]
_ = x[RightClick-131] _ = x[LeftClick-131]
_ = x[SLeftClick-132] _ = x[RightClick-132]
_ = x[SRightClick-133] _ = x[SLeftClick-133]
_ = x[ScrollUp-134] _ = x[SRightClick-134]
_ = x[ScrollDown-135] _ = x[ScrollUp-135]
_ = x[SScrollUp-136] _ = x[ScrollDown-136]
_ = x[SScrollDown-137] _ = x[SScrollUp-137]
_ = x[PreviewScrollUp-138] _ = x[SScrollDown-138]
_ = x[PreviewScrollDown-139] _ = x[PreviewScrollUp-139]
_ = x[Resize-140] _ = x[PreviewScrollDown-140]
_ = x[Change-141] _ = x[Resize-141]
_ = x[BackwardEOF-142] _ = x[Change-142]
_ = x[Start-143] _ = x[BackwardEOF-143]
_ = x[Load-144] _ = x[Start-144]
_ = x[Focus-145] _ = x[Load-145]
_ = x[One-146] _ = x[Focus-146]
_ = x[Zero-147] _ = x[One-147]
_ = x[Result-148] _ = x[Zero-148]
_ = x[Jump-149] _ = x[Result-149]
_ = x[JumpCancel-150] _ = x[Jump-150]
_ = x[ClickHeader-151] _ = x[JumpCancel-151]
_ = x[ClickFooter-152] _ = x[ClickHeader-152]
_ = x[Multi-153] _ = x[ClickFooter-153]
_ = x[Multi-154]
} }
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlBackspaceTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti" const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 52, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 143, 152, 165, 181, 190, 199, 207, 216, 222, 228, 236, 238, 242, 246, 251, 255, 258, 264, 271, 280, 289, 299, 310, 319, 327, 338, 351, 353, 355, 357, 359, 361, 363, 365, 367, 369, 372, 375, 378, 390, 395, 402, 409, 417, 426, 433, 439, 448, 459, 469, 481, 493, 506, 520, 532, 543, 557, 573, 579, 587, 595, 604, 612, 619, 629, 639, 651, 654, 661, 670, 681, 692, 704, 715, 725, 741, 754, 767, 782, 793, 806, 819, 833, 846, 858, 873, 888, 905, 919, 935, 951, 968, 984, 999, 1017, 1035, 1055, 1062, 1067, 1086, 1103, 1108, 1119, 1128, 1138, 1148, 1159, 1167, 1177, 1186, 1197, 1212, 1229, 1235, 1241, 1252, 1257, 1261, 1266, 1269, 1273, 1279, 1283, 1293, 1304, 1315, 1320} 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

@@ -98,7 +98,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
r.flush() r.flush()
var err error var err error
bytes := []byte{} bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ { for tries := range offsetPollTries {
bytes, err = r.getBytesInternal(bytes, tries > 0) bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil { if err != nil {
return -1, -1 return -1, -1

View File

@@ -371,10 +371,12 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
case rune(tcell.KeyCtrlH): case rune(tcell.KeyCtrlH):
switch { switch {
case ctrl:
return keyfn('h')
case alt: case alt:
return Event{AltBackspace, 0, nil} return Event{AltBackspace, 0, nil}
case ctrl, none, shift: case none, shift:
return keyfn('h') return Event{Backspace, 0, nil}
} }
} }
case tcell.KeyCtrlI: case tcell.KeyCtrlI:

View File

@@ -110,21 +110,21 @@ func TestGetCharEventKey(t *testing.T) {
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 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}, wantKey{CtrlBackspace, 0, nil}},
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 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{CtrlBackspace, 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{CtrlBackspace, 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{CtrlBackspace, 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{CtrlAltBackspace, 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{CtrlBackspace, 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{CtrlAltBackspace, 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{CtrlBackspace, 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{AltBackspace, 0, 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{CtrlBackspace, 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
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{AltBackspace, 0, nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke {giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
// 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}},

View File

@@ -43,7 +43,7 @@ const (
CtrlE CtrlE
CtrlF CtrlF
CtrlG CtrlG
CtrlBackspace CtrlH
Tab Tab
CtrlJ CtrlJ
CtrlK CtrlK
@@ -137,6 +137,7 @@ const (
CtrlRight CtrlRight
CtrlHome CtrlHome
CtrlEnd CtrlEnd
CtrlBackspace
CtrlDelete CtrlDelete
CtrlPageUp CtrlPageUp
CtrlPageDown CtrlPageDown
@@ -455,6 +456,7 @@ type ColorTheme struct {
PreviewBg ColorAttr PreviewBg ColorAttr
DarkBg ColorAttr DarkBg ColorAttr
Gutter ColorAttr Gutter ColorAttr
AltGutter ColorAttr
Prompt ColorAttr Prompt ColorAttr
InputBg ColorAttr InputBg ColorAttr
InputBorder ColorAttr InputBorder ColorAttr
@@ -825,6 +827,8 @@ var (
ColCursor ColorPair ColCursor ColorPair
ColCursorEmpty ColorPair ColCursorEmpty ColorPair
ColCursorEmptyChar ColorPair ColCursorEmptyChar ColorPair
ColAltCursorEmpty ColorPair
ColAltCursorEmptyChar ColorPair
ColMarker ColorPair ColMarker ColorPair
ColSelected ColorPair ColSelected ColorPair
ColSelectedMatch ColorPair ColSelectedMatch ColorPair
@@ -890,6 +894,7 @@ func init() {
PreviewFg: defaultColor, PreviewFg: defaultColor,
PreviewBg: defaultColor, PreviewBg: defaultColor,
Gutter: undefined, Gutter: undefined,
AltGutter: undefined,
PreviewBorder: defaultColor, PreviewBorder: defaultColor,
PreviewScrollbar: defaultColor, PreviewScrollbar: defaultColor,
PreviewLabel: defaultColor, PreviewLabel: defaultColor,
@@ -942,6 +947,7 @@ func init() {
PreviewFg: undefined, PreviewFg: undefined,
PreviewBg: undefined, PreviewBg: undefined,
Gutter: undefined, Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined, PreviewBorder: undefined,
PreviewScrollbar: undefined, PreviewScrollbar: undefined,
PreviewLabel: undefined, PreviewLabel: undefined,
@@ -990,6 +996,7 @@ func init() {
PreviewFg: undefined, PreviewFg: undefined,
PreviewBg: undefined, PreviewBg: undefined,
Gutter: undefined, Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined, PreviewBorder: undefined,
PreviewScrollbar: undefined, PreviewScrollbar: undefined,
PreviewLabel: undefined, PreviewLabel: undefined,
@@ -1040,6 +1047,7 @@ func init() {
PreviewFg: undefined, PreviewFg: undefined,
PreviewBg: undefined, PreviewBg: undefined,
Gutter: undefined, Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined, PreviewBorder: undefined,
PreviewScrollbar: undefined, PreviewScrollbar: undefined,
PreviewLabel: undefined, PreviewLabel: undefined,
@@ -1090,6 +1098,7 @@ func init() {
PreviewFg: undefined, PreviewFg: undefined,
PreviewBg: undefined, PreviewBg: undefined,
Gutter: undefined, Gutter: undefined,
AltGutter: undefined,
PreviewBorder: undefined, PreviewBorder: undefined,
PreviewScrollbar: undefined, PreviewScrollbar: undefined,
PreviewLabel: undefined, PreviewLabel: undefined,
@@ -1207,6 +1216,7 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlac
gutter.Attr = Dim gutter.Attr = Dim
} }
theme.Gutter = o(theme.DarkBg, gutter) theme.Gutter = o(theme.DarkBg, gutter)
theme.AltGutter = o(theme.Gutter, theme.AltGutter)
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)
@@ -1276,6 +1286,8 @@ func initPalette(theme *ColorTheme) {
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) ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
ColAltCursorEmpty = pair(blank, theme.AltGutter)
ColAltCursorEmptyChar = pair(theme.AltGutter, 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 {

View File

@@ -8,7 +8,7 @@ import (
func TestAtExit(t *testing.T) { func TestAtExit(t *testing.T) {
want := []int{3, 2, 1, 0} want := []int{3, 2, 1, 0}
var called []int var called []int
for i := 0; i < 4; i++ { for i := range 4 {
n := i n := i
AtExit(func() { called = append(called, n) }) AtExit(func() { called = append(called, n) })
} }

View File

@@ -52,7 +52,7 @@ func ToChars(bytes []byte) Chars {
} }
runes := make([]rune, bytesUntil, len(bytes)) runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ { for i := range bytesUntil {
runes[i] = rune(bytes[i]) runes[i] = rune(bytes[i])
} }
for i := bytesUntil; i < len(bytes); { for i := bytesUntil; i < len(bytes); {
@@ -259,7 +259,7 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
lines = append(lines, text) lines = append(lines, text)
} else { } else {
from := 0 from := 0
for off := 0; off < len(text); off++ { for off := range text {
if text[off] == '\n' { if text[off] == '\n' {
lines = append(lines, text[from:off+1]) // Include '\n' lines = append(lines, text[from:off+1]) // Include '\n'
from = off + 1 from = off + 1

View File

@@ -1190,6 +1190,61 @@ class TestCore < TestInteractive
tmux.until { |lines| assert lines.any_include?('9999␊10000') } tmux.until { |lines| assert lines.any_include?('9999␊10000') }
end end
def test_freeze_left_keep_right
tmux.send_keys %[seq 10000 | #{FZF} --read0 --delimiter "\n" --freeze-left 3 --keep-right --ellipsis XX --no-multi-line --bind space:toggle-multi-line], :Enter
tmux.until { |lines| assert_match(/^> 1␊2␊3XX.*10000␊$/, lines[-3]) }
tmux.send_keys '5'
tmux.until { |lines| assert_match(/^> 1␊2␊3␊4␊5␊.*XX$/, lines[-3]) }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('> 1') }
tmux.send_keys :Space
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
end
def test_freeze_left_and_right
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX], :Enter
tmux.until { |lines| assert_match(/XX9998 9999 10000$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 2 3XX.*XX9998 9999 10000$/,lines[-3]) }
end
def test_freeze_left_and_right_delimiter
tmux.send_keys %[seq 10000 | tr "\n" ' ' | sed 's/ / , /g' | #{FZF} --freeze-left 3 --freeze-right 3 --ellipsis XX --delimiter ' , '], :Enter
tmux.until { |lines| assert_match(/XX, 9999 , 10000 ,$/, lines[-3]) }
tmux.send_keys "'1000"
tmux.until { |lines| assert_match(/^> 1 , 2 , 3 ,XX.*XX, 9999 , 10000 ,$/,lines[-3]) }
end
def test_freeze_right_exceed_range
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-right 100000 --ellipsis XX], :Enter
['', "'1000"].each do |query|
tmux.send_keys query
tmux.until { |lines| assert lines.any_include?("> #{query}".strip) }
tmux.until do |lines|
assert_match(/ 9998 9999 10000$/, lines[-3])
assert_equal(1, lines[-3].scan('XX').size)
end
end
end
def test_freeze_right_exceed_range_with_freeze_left
tmux.send_keys %[seq 10000 | tr "\n" ' ' | #{FZF} --freeze-left 3 --freeze-right 100000 --ellipsis XX], :Enter
tmux.until do |lines|
assert_match(/^> 1 2 3XX.*9998 9999 10000$/, lines[-3])
assert_equal(1, lines[-3].scan('XX').size)
end
end
def test_freeze_right_with_ellipsis_and_scrolling
tmux.send_keys "{ seq 6; ruby -e 'print \"g\"*1000, \"\\n\"'; seq 8 100; } | #{FZF} --ellipsis='777' --freeze-right 1 --scroll-off 0 --bind a:offset-up", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys(*Array.new(6) { :a })
tmux.until do |lines|
assert_match(/> 777g+$/, lines[-3])
assert_equal 1, lines.count { |l| l.end_with?('g') }
end
end
def test_backward_eof def test_backward_eof
tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter tmux.send_keys "echo foo | #{FZF} --bind 'backward-eof:reload(seq 100)'", :Enter
tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 } tmux.until { |lines| lines.item_count == 1 && lines.match_count == 1 }

View File

@@ -1215,6 +1215,15 @@ class TestLayout < TestInteractive
end end
end end
def test_header_and_footer_should_not_be_wider_than_list
tmux.send_keys %(WIDE=$(printf 'x%.0s' {1..1000}); (echo $WIDE; echo $WIDE) | fzf --header-lines 1 --style full --header-border bottom --header-lines-border top --ellipsis XX --header "$WIDE" --footer "$WIDE" --no-footer-border), :Enter
tmux.until do |lines|
matches = lines.filter_map { |line| line[/x+XX/] }
assert_equal 4, matches.length
assert_equal 1, matches.uniq.length
end
end
def test_combinations def test_combinations
skip unless ENV['LONGTEST'] skip unless ENV['LONGTEST']

View File

@@ -462,6 +462,84 @@ class TestZsh < TestBase
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
end end
end end
# Helper function to run test with Perl and again with Awk
def self.test_perl_and_awk(name, &block)
define_method("test_#{name}") do
instance_eval(&block)
end
define_method("test_#{name}_awk") do
tmux.send_keys "unset 'commands[perl]'", :Enter
tmux.prepare
# Verify perl is actually unset (0 = not found)
tmux.send_keys 'echo ${+commands[perl]}', :Enter
tmux.until { |lines| assert_equal '0', lines[-1] }
tmux.prepare
instance_eval(&block)
end
end
def prepare_ctrl_r_test
tmux.send_keys ':', :Enter
tmux.send_keys 'echo match-collision', :Enter
tmux.prepare
tmux.send_keys 'echo "line 1', :Enter, '2 line 2"', :Enter
tmux.prepare
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
tmux.prepare
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
tmux.prepare
tmux.send_keys 'echo "trailing_space "', :Enter
tmux.prepare
tmux.send_keys 'cat <<EOF | wc -c', :Enter, 'qux thud', :Enter, 'EOF', :Enter
tmux.prepare
tmux.send_keys 'C-l', 'C-r'
end
test_perl_and_awk 'ctrl_r_accept_or_print_query' do
set_var('FZF_CTRL_R_OPTS', '--bind enter:accept-or-print-query')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys '1 foobar'
tmux.until { |lines| assert_equal 0, lines.match_count }
tmux.send_keys :Enter
tmux.until { |lines| assert_equal '1 foobar', lines[-1] }
end
test_perl_and_awk 'ctrl_r_multiline_index_collision' do
# Leading number in multi-line history content is not confused with index
prepare_ctrl_r_test
tmux.send_keys "'line 1"
tmux.until { |lines| assert_equal 1, lines.match_count }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['echo "line 1', '2 line 2"'], lines[-2..]
end
end
test_perl_and_awk 'ctrl_r_multi_selection' do
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| assert_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF', 'echo "trailing_space "', 'echo "bar', 'foo"'], lines[-6..]
end
end
test_perl_and_awk 'ctrl_r_no_multi_selection' do
set_var('FZF_CTRL_R_OPTS', '--no-multi')
prepare_ctrl_r_test
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| refute_includes lines[-2], '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
assert_equal ['cat <<EOF | wc -c', 'qux thud', 'EOF'], lines[-3..]
end
end
end end
class TestFish < TestBase class TestFish < TestBase