m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-10 20:33:48 -05:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Massimo Mund
ead534a1be Fix modifier detection for Backspace / Ctrl-H on Windows (#4582)
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
Fix #4578
2025-11-09 15:36:07 +09:00
6 changed files with 68 additions and 220 deletions

View File

@@ -1,24 +1,6 @@
CHANGELOG CHANGELOG
========= =========
0.67.0
------
- Added `--freeze-left=N` option to keep the leftmost N columns visible.
```sh
# Keeps the file name column fixed and always visible
git grep --line-number --color=always -- '' |
fzf --ansi --delimiter : --freeze-left 1
# 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 visible.
```sh
fd | fzf --freeze-right 1 --delimiter /
fd | fzf --freeze-left 1 --freeze-right 1 --delimiter /
```
0.66.1 0.66.1
------ ------
- Bug fixes - Bug fixes

View File

@@ -629,16 +629,9 @@ 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. Use \fB\-\-freeze\-right=1\fR instead if you want the query string is empty.
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

View File

@@ -104,8 +104,6 @@ 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)
@@ -564,8 +562,6 @@ 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
@@ -2699,14 +2695,6 @@ 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 {
@@ -3350,10 +3338,6 @@ 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

@@ -331,8 +331,6 @@ 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
@@ -498,6 +496,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
@@ -1052,8 +1058,6 @@ 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,
@@ -3529,34 +3533,6 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
} }
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]
splitOffset2 = int(token.prefixLength) + token.text.Length()
}
splitOffset2 = util.Max(splitOffset2, splitOffset1)
}
}
maxLines := 1 maxLines := 1
if t.canSpanMultiLines() { if t.canSpanMultiLines() {
maxLines = maxLineNum - lineNum + 1 maxLines = maxLineNum - lineNum + 1
@@ -3626,24 +3602,16 @@ 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 maxEnd int var maxe int
for _, offset := range offsets { for _, offset := range offsets {
if offset.match { if offset.match {
maxEnd = util.Max(maxEnd, int(offset.offset[1])) maxe = util.Max(maxe, int(offset.offset[1]))
} }
} }
@@ -3707,112 +3675,69 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
wrapped = true wrapped = true
} }
frozenLeft := line[:splitOffsetLeft] displayWidth = t.displayWidthWithLimit(line, 0, maxWidth)
middle := line[splitOffsetLeft:] if !t.wrap && displayWidth > maxWidth {
frozenRight := []rune{} ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
if splitOffsetRight >= splitOffsetLeft { maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(line))
middle = line[splitOffsetLeft:splitOffsetRight] transformOffsets := func(diff int32, rightTrim bool) {
frozenRight = line[splitOffsetRight:] for idx, offset := range offsets {
} b, e := offset.offset[0], offset.offset[1]
displayWidthSum := 0 el := int32(len(ellipsis))
todo := [3]func(){} b += el - diff
for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} { e += el - diff
if len(runes) == 0 { b = util.Max32(b, el)
continue if rightTrim {
} e = util.Min32(e, int32(maxWidth-ellipsisWidth))
shift := 0
maxe := maxEnd
offs := make([]colorOffset, len(offsets))
for idx := range offsets {
offs[idx] = offsets[idx]
if fidx == 1 && splitOffsetRight > 0 {
shift = splitOffsetRight
} else if fidx == 2 && splitOffsetLeft > 0 {
shift = splitOffsetLeft
}
offs[idx].offset[0] -= int32(shift)
offs[idx].offset[1] -= int32(shift)
}
maxe -= shift
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
if !t.wrap && displayWidth > maxWidth {
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
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)
} }
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
} }
if t.hscroll { }
if fidx == 1 || fidx == 2 && t.keepRight && pos == nil { if t.hscroll {
trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth) if t.keepRight && pos == nil {
transformOffsets(diff, false) trimmed, diff := t.trimLeft(line, maxWidth, ellipsisWidth)
runes = append(ellipsis, trimmed...) transformOffsets(diff, false)
} else if fidx == 0 || !t.overflow(runes[:maxe], maxWidth-ellipsisWidth) { line = append(ellipsis, trimmed...)
// Stri.. } else if !t.overflow(line[:maxe], maxWidth-ellipsisWidth) {
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth) // Stri..
runes = append(runes, ellipsis...) line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
} else { line = append(line, ellipsis...)
// 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
transformOffsets(diff, rightTrim)
runes = append(ellipsis, runes...)
}
} else { } else {
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth) // Stri..
runes = append(runes, ellipsis...) rightTrim := false
if t.overflow(line[maxe:], ellipsisWidth) {
for idx, offset := range offs { line = append(line[:maxe], ellipsis...)
offs[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis))) rightTrim = true
offs[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
} }
} // ..ri..
displayWidth = t.displayWidthWithLimit(runes, 0, displayWidth) var diff int32
} line, diff = t.trimLeft(line, maxWidth, ellipsisWidth)
displayWidthSum += displayWidth
if maxWidth > 0 { // Transform offsets
color := colBase transformOffsets(diff, rightTrim)
if hidden { line = append(ellipsis, line...)
color = color.WithFg(t.theme.Nomatch)
}
todo[fidx] = func() {
t.printColoredString(t.window, runes, offs, color)
} }
} else { } else {
break line, _ = t.trimRight(line, maxWidth-ellipsisWidth)
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))
}
} }
maxWidth -= displayWidth displayWidth = t.displayWidthWithLimit(line, 0, displayWidth)
} }
if todo[0] != nil {
todo[0]() if maxWidth > 0 {
} color := colBase
if todo[2] != nil { if hidden {
todo[2]() color = color.WithFg(t.theme.Nomatch)
} }
if todo[1] != nil { t.printColoredString(t.window, line, offsets, color)
todo[1]()
} }
if postTask != nil { if postTask != nil {
postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg) postTask(actualLineNum, displayWidth, wasWrapped, forceRedraw, lbg)
} else { } else {
t.markOtherLine(actualLineNum) t.markOtherLine(actualLineNum)
} }
@@ -5611,7 +5536,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
} }
} }

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

@@ -1190,44 +1190,6 @@ 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_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_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 }