mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 06:43:47 -05:00
Add --freeze-right=N option to keep the rightmost N fields visible
This commit is contained in:
@@ -13,6 +13,11 @@ CHANGELOG
|
|||||||
git grep --line-number --color=always -- '' |
|
git grep --line-number --color=always -- '' |
|
||||||
fzf --ansi --delimiter : --freeze-left 1 --keep-right
|
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
|
||||||
------
|
------
|
||||||
|
|||||||
@@ -632,9 +632,13 @@ The given string will be repeated to draw a horizontal line on each gap
|
|||||||
.BI "\-\-freeze\-left=" "N"
|
.BI "\-\-freeze\-left=" "N"
|
||||||
Number of fields to freeze on the left.
|
Number of fields to freeze on the left.
|
||||||
.TP
|
.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
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ Usage: fzf [options]
|
|||||||
--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-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,6 +565,7 @@ type Options struct {
|
|||||||
Normalize bool
|
Normalize bool
|
||||||
Nth []Range
|
Nth []Range
|
||||||
FreezeLeft int
|
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
|
||||||
@@ -2701,6 +2703,10 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
|||||||
if opts.FreezeLeft, err = nextInt("number of fields required"); err != nil {
|
if opts.FreezeLeft, err = nextInt("number of fields required"); err != nil {
|
||||||
return err
|
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 {
|
||||||
@@ -3344,7 +3350,7 @@ 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 {
|
if opts.FreezeLeft < 0 || opts.FreezeRight < 0 {
|
||||||
return errors.New("number of fields to freeze must be a non-negative integer")
|
return errors.New("number of fields to freeze must be a non-negative integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
src/terminal.go
100
src/terminal.go
@@ -332,6 +332,7 @@ type Terminal struct {
|
|||||||
previewScrollbar string
|
previewScrollbar string
|
||||||
ansi bool
|
ansi bool
|
||||||
freezeLeft int
|
freezeLeft int
|
||||||
|
freezeRight int
|
||||||
nthAttr tui.Attr
|
nthAttr tui.Attr
|
||||||
nth []Range
|
nth []Range
|
||||||
nthCurrent []Range
|
nthCurrent []Range
|
||||||
@@ -1052,6 +1053,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
|||||||
header0: opts.Header,
|
header0: opts.Header,
|
||||||
ansi: opts.Ansi,
|
ansi: opts.Ansi,
|
||||||
freezeLeft: opts.FreezeLeft,
|
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,
|
||||||
@@ -3528,14 +3530,30 @@ 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
|
// Determine split offset for horizontal scrolling with freeze
|
||||||
splitOffset := -1
|
splitOffset1 := -1
|
||||||
if t.hscroll && !t.wrap && t.freezeLeft > 0 {
|
splitOffset2 := -1
|
||||||
tokens := Tokenize(item.text.ToString(), t.delimiter)
|
if t.hscroll && !t.wrap {
|
||||||
if len(tokens) == 0 {
|
var tokens []Token
|
||||||
splitOffset = 0
|
if t.freezeLeft > 0 || t.freezeRight > 0 {
|
||||||
} else {
|
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]
|
token := tokens[util.Min(t.freezeLeft, len(tokens))-1]
|
||||||
splitOffset = int(token.prefixLength) + token.text.Length() - token.text.TrailingWhitespaces()
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3608,9 +3626,13 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
splitOffsetLocal := 0
|
splitOffsetLeft := 0
|
||||||
if splitOffset >= 0 && splitOffset > from && splitOffset < from+len(line) {
|
if splitOffset1 >= 0 && splitOffset1 > from && splitOffset1 < from+len(line) {
|
||||||
splitOffsetLocal = splitOffset - from
|
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 {
|
||||||
@@ -3618,10 +3640,10 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
}
|
}
|
||||||
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]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3685,26 +3707,39 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
wrapped = true
|
wrapped = true
|
||||||
}
|
}
|
||||||
|
|
||||||
frozen := line[:splitOffsetLocal]
|
frozenLeft := line[:splitOffsetLeft]
|
||||||
rest := line[splitOffsetLocal:]
|
middle := line[splitOffsetLeft:]
|
||||||
|
frozenRight := []rune{}
|
||||||
|
if splitOffsetRight >= splitOffsetLeft {
|
||||||
|
middle = line[splitOffsetLeft:splitOffsetRight]
|
||||||
|
frozenRight = line[splitOffsetRight:]
|
||||||
|
}
|
||||||
displayWidthSum := 0
|
displayWidthSum := 0
|
||||||
for fidx, runes := range [][]rune{frozen, rest} {
|
todo := [3]func(){}
|
||||||
|
for fidx, runes := range [][]rune{frozenLeft, frozenRight, middle} {
|
||||||
if len(runes) == 0 {
|
if len(runes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if splitOffsetLocal > 0 && fidx == 1 {
|
shift := 0
|
||||||
|
maxe := maxEnd
|
||||||
|
offs := make([]colorOffset, len(offsets))
|
||||||
for idx := range offsets {
|
for idx := range offsets {
|
||||||
offsets[idx].offset[0] -= int32(splitOffsetLocal)
|
offs[idx] = offsets[idx]
|
||||||
offsets[idx].offset[1] -= int32(splitOffsetLocal)
|
if fidx == 1 && splitOffsetRight > 0 {
|
||||||
|
shift = splitOffsetRight
|
||||||
|
} else if fidx == 2 && splitOffsetLeft > 0 {
|
||||||
|
shift = splitOffsetLeft
|
||||||
}
|
}
|
||||||
maxe -= splitOffsetLocal
|
offs[idx].offset[0] -= int32(shift)
|
||||||
|
offs[idx].offset[1] -= int32(shift)
|
||||||
}
|
}
|
||||||
|
maxe -= shift
|
||||||
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
|
displayWidth = t.displayWidthWithLimit(runes, 0, maxWidth)
|
||||||
if !t.wrap && displayWidth > maxWidth {
|
if !t.wrap && displayWidth > maxWidth {
|
||||||
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
|
ellipsis, ellipsisWidth := util.Truncate(t.ellipsis, maxWidth/2)
|
||||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
|
maxe = util.Constrain(maxe+util.Min(maxWidth/2-ellipsisWidth, t.hscrollOff), 0, len(runes))
|
||||||
transformOffsets := func(diff int32, rightTrim bool) {
|
transformOffsets := func(diff int32, rightTrim bool) {
|
||||||
for idx, offset := range offsets {
|
for idx, offset := range offs {
|
||||||
b, e := offset.offset[0], offset.offset[1]
|
b, e := offset.offset[0], offset.offset[1]
|
||||||
el := int32(len(ellipsis))
|
el := int32(len(ellipsis))
|
||||||
b += el - diff
|
b += el - diff
|
||||||
@@ -3713,12 +3748,12 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
if rightTrim {
|
if rightTrim {
|
||||||
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
e = util.Min32(e, int32(maxWidth-ellipsisWidth))
|
||||||
}
|
}
|
||||||
offsets[idx].offset[0] = b
|
offs[idx].offset[0] = b
|
||||||
offsets[idx].offset[1] = util.Max32(b, e)
|
offs[idx].offset[1] = util.Max32(b, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if t.hscroll {
|
if t.hscroll {
|
||||||
if fidx > 0 && t.keepRight && pos == nil {
|
if fidx == 1 || fidx == 2 && t.keepRight && pos == nil {
|
||||||
trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth)
|
trimmed, diff := t.trimLeft(runes, maxWidth, ellipsisWidth)
|
||||||
transformOffsets(diff, false)
|
transformOffsets(diff, false)
|
||||||
runes = append(ellipsis, trimmed...)
|
runes = append(ellipsis, trimmed...)
|
||||||
@@ -3745,9 +3780,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
|
runes, _ = t.trimRight(runes, maxWidth-ellipsisWidth)
|
||||||
runes = append(runes, ellipsis...)
|
runes = append(runes, ellipsis...)
|
||||||
|
|
||||||
for idx, offset := range offsets {
|
for idx, offset := range offs {
|
||||||
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
offs[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-len(ellipsis)))
|
||||||
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
offs[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
displayWidth = t.displayWidthWithLimit(runes, 0, displayWidth)
|
displayWidth = t.displayWidthWithLimit(runes, 0, displayWidth)
|
||||||
@@ -3759,12 +3794,23 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
|||||||
if hidden {
|
if hidden {
|
||||||
color = color.WithFg(t.theme.Nomatch)
|
color = color.WithFg(t.theme.Nomatch)
|
||||||
}
|
}
|
||||||
t.printColoredString(t.window, runes, offsets, color)
|
todo[fidx] = func() {
|
||||||
|
t.printColoredString(t.window, runes, offs, color)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
maxWidth -= displayWidth
|
maxWidth -= displayWidth
|
||||||
}
|
}
|
||||||
|
if todo[0] != nil {
|
||||||
|
todo[0]()
|
||||||
|
}
|
||||||
|
if todo[2] != nil {
|
||||||
|
todo[2]()
|
||||||
|
}
|
||||||
|
if todo[1] != nil {
|
||||||
|
todo[1]()
|
||||||
|
}
|
||||||
if postTask != nil {
|
if postTask != nil {
|
||||||
postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg)
|
postTask(actualLineNum, displayWidthSum, wasWrapped, forceRedraw, lbg)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1201,6 +1201,33 @@ class TestCore < TestInteractive
|
|||||||
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
|
tmux.until { |lines| assert lines.any_include?('1␊2␊3␊4␊5␊') }
|
||||||
end
|
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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user