mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-11-25 06:07:42 +00:00
Add support for preview window header
Fix #2373 # Display top 3 lines as the fixed header fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
This commit is contained in:
parent
7310370a31
commit
4c4c6e626e
@ -1,10 +1,17 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
0.25.2
|
0.26.0
|
||||||
------
|
------
|
||||||
|
- Added support for fixed header in preview window
|
||||||
|
```sh
|
||||||
|
# Display top 3 lines as the fixed header
|
||||||
|
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
|
||||||
|
```
|
||||||
- Added `select` and `deselect` action for unconditionally selecting or
|
- Added `select` and `deselect` action for unconditionally selecting or
|
||||||
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
||||||
|
- Sigificant performance improvement in ANSI code processing
|
||||||
|
- Bug fixes and improvements
|
||||||
- Built with Go 1.16
|
- Built with Go 1.16
|
||||||
|
|
||||||
0.25.1
|
0.25.1
|
||||||
|
@ -442,7 +442,7 @@ e.g.
|
|||||||
done'\fR
|
done'\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
|
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:~HEADER_LINES][:default]"
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
.B POSITION: (default: right)
|
.B POSITION: (default: right)
|
||||||
@ -487,6 +487,9 @@ for adjusting the base offset so that you can see the text above it. It should
|
|||||||
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
||||||
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
||||||
|
|
||||||
|
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
|
||||||
|
are always visible.
|
||||||
|
|
||||||
* \fBdefault\fR resets all options previously set to the default.
|
* \fBdefault\fR resets all options previously set to the default.
|
||||||
|
|
||||||
.RS
|
.RS
|
||||||
@ -506,6 +509,8 @@ e.g.
|
|||||||
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
||||||
--preview-window +{2}-/2\fR
|
--preview-window +{2}-/2\fR
|
||||||
|
|
||||||
|
# Display top 3 lines as the fixed header
|
||||||
|
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
.SS Scripting
|
.SS Scripting
|
||||||
|
@ -84,7 +84,7 @@ const usage = `usage: fzf [options]
|
|||||||
[up|down|left|right][:SIZE[%]]
|
[up|down|left|right][:SIZE[%]]
|
||||||
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
|
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
|
||||||
[:rounded|sharp|noborder]
|
[:rounded|sharp|noborder]
|
||||||
[:+SCROLL[-OFFSET]]
|
[:+SCROLL[-OFFSET]][:~HEADER_LINES]
|
||||||
[:default]
|
[:default]
|
||||||
|
|
||||||
Scripting
|
Scripting
|
||||||
@ -161,15 +161,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type previewOpts struct {
|
type previewOpts struct {
|
||||||
command string
|
command string
|
||||||
position windowPosition
|
position windowPosition
|
||||||
size sizeSpec
|
size sizeSpec
|
||||||
scroll string
|
scroll string
|
||||||
hidden bool
|
hidden bool
|
||||||
wrap bool
|
wrap bool
|
||||||
cycle bool
|
cycle bool
|
||||||
follow bool
|
follow bool
|
||||||
border tui.BorderShape
|
border tui.BorderShape
|
||||||
|
headerLines int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options stores the values of command-line options
|
// Options stores the values of command-line options
|
||||||
@ -231,7 +232,7 @@ type Options struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultPreviewOpts(command string) previewOpts {
|
func defaultPreviewOpts(command string) previewOpts {
|
||||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
|
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultOptions() *Options {
|
func defaultOptions() *Options {
|
||||||
@ -1078,6 +1079,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
|||||||
tokens := strings.Split(input, ":")
|
tokens := strings.Split(input, ":")
|
||||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||||
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
|
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
|
||||||
|
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
switch token {
|
switch token {
|
||||||
case "":
|
case "":
|
||||||
@ -1114,7 +1116,9 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
|||||||
case "nofollow":
|
case "nofollow":
|
||||||
opts.follow = false
|
opts.follow = false
|
||||||
default:
|
default:
|
||||||
if sizeRegex.MatchString(token) {
|
if headerRegex.MatchString(token) {
|
||||||
|
opts.headerLines = atoi(token[1:])
|
||||||
|
} else if sizeRegex.MatchString(token) {
|
||||||
opts.size = parseSize(token, 99, "window size")
|
opts.size = parseSize(token, 99, "window size")
|
||||||
} else if offsetRegex.MatchString(token) {
|
} else if offsetRegex.MatchString(token) {
|
||||||
opts.scroll = token[1:]
|
opts.scroll = token[1:]
|
||||||
@ -1364,7 +1368,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Preview.command = ""
|
opts.Preview.command = ""
|
||||||
case "--preview-window":
|
case "--preview-window":
|
||||||
parsePreviewWindow(&opts.Preview,
|
parsePreviewWindow(&opts.Preview,
|
||||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]"))
|
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:~HEADER_LINES][:default]"))
|
||||||
case "--height":
|
case "--height":
|
||||||
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
||||||
case "--min-height":
|
case "--min-height":
|
||||||
|
@ -1295,18 +1295,37 @@ func (t *Terminal) renderPreviewSpinner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) renderPreviewText(unchanged bool) {
|
func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||||
maxWidth := t.pwindow.Width()
|
|
||||||
lineNo := -t.previewer.offset
|
|
||||||
height := t.pwindow.Height()
|
|
||||||
if unchanged {
|
if unchanged {
|
||||||
t.pwindow.MoveAndClear(0, 0)
|
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
|
||||||
} else {
|
} else {
|
||||||
t.previewed.filled = false
|
t.previewed.filled = false
|
||||||
t.pwindow.Erase()
|
t.pwindow.Erase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
height := t.pwindow.Height()
|
||||||
|
header := []string{}
|
||||||
|
body := t.previewer.lines
|
||||||
|
headerLines := t.previewOpts.headerLines
|
||||||
|
// Do not enable preview header lines if it's value is too large
|
||||||
|
if headerLines > 0 && headerLines < util.Min(len(body), height) {
|
||||||
|
header = t.previewer.lines[0:headerLines]
|
||||||
|
body = t.previewer.lines[headerLines:]
|
||||||
|
// Always redraw header
|
||||||
|
t.renderPreviewText(height, header, 0, false)
|
||||||
|
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||||
|
}
|
||||||
|
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
|
||||||
|
|
||||||
|
if !unchanged {
|
||||||
|
t.pwindow.FinishFill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||||
|
maxWidth := t.pwindow.Width()
|
||||||
var ansi *ansiState
|
var ansi *ansiState
|
||||||
for _, line := range t.previewer.lines {
|
for _, line := range lines {
|
||||||
var lbg tui.Color = -1
|
var lbg tui.Color = -1
|
||||||
if ansi != nil {
|
if ansi != nil {
|
||||||
ansi.lbg = -1
|
ansi.lbg = -1
|
||||||
@ -1354,9 +1373,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
|||||||
}
|
}
|
||||||
lineNo++
|
lineNo++
|
||||||
}
|
}
|
||||||
if !unchanged {
|
|
||||||
t.pwindow.FinishFill()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPreview() {
|
func (t *Terminal) printPreview() {
|
||||||
@ -1369,7 +1385,7 @@ func (t *Terminal) printPreview() {
|
|||||||
t.previewer.version == t.previewed.version &&
|
t.previewer.version == t.previewed.version &&
|
||||||
t.previewer.offset == t.previewed.offset
|
t.previewer.offset == t.previewed.offset
|
||||||
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||||
t.renderPreviewText(unchanged)
|
t.renderPreviewArea(unchanged)
|
||||||
t.renderPreviewSpinner()
|
t.renderPreviewSpinner()
|
||||||
t.previewed.numLines = numLines
|
t.previewed.numLines = numLines
|
||||||
t.previewed.version = t.previewer.version
|
t.previewed.version = t.previewer.version
|
||||||
@ -1382,7 +1398,7 @@ func (t *Terminal) printPreviewDelayed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.previewer.scrollable = false
|
t.previewer.scrollable = false
|
||||||
t.renderPreviewText(true)
|
t.renderPreviewArea(true)
|
||||||
|
|
||||||
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
||||||
pos := t.pwindow.Width() - len(message)
|
pos := t.pwindow.Width() - len(message)
|
||||||
@ -1929,7 +1945,7 @@ func (t *Terminal) Loop() {
|
|||||||
cmd := util.ExecCommand(command, true)
|
cmd := util.ExecCommand(command, true)
|
||||||
if pwindow != nil {
|
if pwindow != nil {
|
||||||
height := pwindow.Height()
|
height := pwindow.Height()
|
||||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
|
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
lines := fmt.Sprintf("LINES=%d", height)
|
lines := fmt.Sprintf("LINES=%d", height)
|
||||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||||
@ -2132,7 +2148,7 @@ func (t *Terminal) Loop() {
|
|||||||
if t.previewer.following {
|
if t.previewer.following {
|
||||||
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
||||||
} else if result.offset >= 0 {
|
} else if result.offset >= 0 {
|
||||||
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
|
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
|
||||||
}
|
}
|
||||||
t.printPreview()
|
t.printPreview()
|
||||||
case reqPreviewRefresh:
|
case reqPreviewRefresh:
|
||||||
@ -2205,7 +2221,7 @@ func (t *Terminal) Loop() {
|
|||||||
if t.previewOpts.cycle {
|
if t.previewOpts.cycle {
|
||||||
newOffset = (newOffset + numLines) % numLines
|
newOffset = (newOffset + numLines) % numLines
|
||||||
}
|
}
|
||||||
newOffset = util.Constrain(newOffset, 0, numLines-1)
|
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
|
||||||
if t.previewer.offset != newOffset {
|
if t.previewer.offset != newOffset {
|
||||||
t.previewer.offset = newOffset
|
t.previewer.offset = newOffset
|
||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
|
@ -2016,6 +2016,32 @@ class TestGoFZF < TestBase
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_preview_header
|
||||||
|
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||||
|
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||||
|
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_includes lines[1], '4/1000'
|
||||||
|
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys '55'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 1, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-J'
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys :BSpace
|
||||||
|
tmux.until do |lines|
|
||||||
|
assert_equal 19, lines.match_count
|
||||||
|
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||||
|
end
|
||||||
|
tmux.send_keys 'C-K'
|
||||||
|
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestShell
|
module TestShell
|
||||||
|
Loading…
Reference in New Issue
Block a user