diff --git a/CHANGELOG.md b/CHANGELOG.md index 13f0615..2b2d20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ CHANGELOG ========= -0.27.4 +0.28.0 ------ +- Added `--header-first` option to print header before the prompt line + ```sh + fzf --header $'Welcome to fzf\n▔▔▔▔▔▔▔▔▔▔▔▔▔▔' --reverse --height 30% --border --header-first + ``` - Added `--scroll-off=LINES` option (similar to `scrolloff` option of Vim) - You can set it to a very large number so that the cursor stays in the middle of the screen while scrolling diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 975018b..b4721d0 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 THE SOFTWARE. .. -.TH fzf 1 "Nov 2021" "fzf 0.27.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2021" "fzf 0.28.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -299,6 +299,9 @@ are not affected by \fB--with-nth\fR. ANSI color codes are processed even when The first N lines of the input are treated as the sticky header. When \fB--with-nth\fR is set, the lines are transformed just like the other lines that follow. +.TP +.B "--header-first" +Print header before the prompt line .SS Display .TP .B "--ansi" diff --git a/src/options.go b/src/options.go index daa4fe0..e2c03dd 100644 --- a/src/options.go +++ b/src/options.go @@ -69,6 +69,7 @@ const usage = `usage: fzf [options] --marker=STR Multi-select marker (default: '>') --header=STR String to print as header --header-lines=N The first N lines of the input are treated as header + --header-first Print header before the prompt line Display --ansi Enable processing of ANSI color codes @@ -225,6 +226,7 @@ type Options struct { History *History Header []string HeaderLines int + HeaderFirst bool Margin [4]sizeSpec Padding [4]sizeSpec BorderShape tui.BorderShape @@ -287,6 +289,7 @@ func defaultOptions() *Options { History: nil, Header: make([]string, 0), HeaderLines: 0, + HeaderFirst: false, Margin: defaultMargin(), Padding: defaultMargin(), Unicode: true, @@ -1427,6 +1430,10 @@ func parseOptions(opts *Options, allArgs []string) { case "--header-lines": opts.HeaderLines = atoi( nextString(allArgs, &i, "number of header lines required")) + case "--header-first": + opts.HeaderFirst = true + case "--no-header-first": + opts.HeaderFirst = false case "--preview": opts.Preview.command = nextString(allArgs, &i, "preview command required") case "--no-preview": diff --git a/src/terminal.go b/src/terminal.go index 1af399a..2bad5d7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -140,6 +140,8 @@ type Terminal struct { printQuery bool history *History cycle bool + headerFirst bool + headerLines int header []string header0 []string ansi bool @@ -529,6 +531,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { paused: opts.Phony, strong: strongAttr, cycle: opts.Cycle, + headerFirst: opts.HeaderFirst, + headerLines: opts.HeaderLines, header: header, header0: header, ansi: opts.Ansi, @@ -976,12 +980,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) { return before, after } +func (t *Terminal) promptLine() int { + if t.headerFirst { + max := t.window.Height() - 1 + if !t.noInfoLine() { + max-- + } + return util.Min(len(t.header0)+t.headerLines, max) + } + return 0 +} + func (t *Terminal) placeCursor() { - t.move(0, t.promptLen+t.queryLen[0], false) + t.move(t.promptLine(), t.promptLen+t.queryLen[0], false) } func (t *Terminal) printPrompt() { - t.move(0, 0, true) + t.move(t.promptLine(), 0, true) t.prompt() before, after := t.updatePromptOffset() @@ -1003,22 +1018,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string { func (t *Terminal) printInfo() { pos := 0 + line := t.promptLine() switch t.infoStyle { case infoDefault: - t.move(1, 0, true) + t.move(line+1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration t.window.CPrint(tui.ColSpinner, t.spinner[idx]) } - t.move(1, 2, false) + t.move(line+1, 2, false) pos = 2 case infoInline: pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 if pos+len(" < ") > t.window.Width() { return } - t.move(0, pos, true) + t.move(line, pos, true) if t.reading { t.window.CPrint(tui.ColSpinner, " < ") } else { @@ -1061,11 +1077,20 @@ func (t *Terminal) printHeader() { return } max := t.window.Height() + if t.headerFirst { + max-- + if !t.noInfoLine() { + max-- + } + } var state *ansiState for idx, lineStr := range t.header { - line := idx + 2 - if t.noInfoLine() { - line-- + line := idx + if !t.headerFirst { + line++ + if !t.noInfoLine() { + line++ + } } if line >= max { continue @@ -2644,7 +2669,7 @@ func (t *Terminal) Loop() { } } } else if me.Down { - if my == 0 && mx >= 0 { + if my == t.promptLine() && mx >= 0 { // Prompt t.cx = mx + t.xoffset } else if my >= min { diff --git a/test/test_go.rb b/test/test_go.rb index e82a85c..20a4c92 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2109,6 +2109,39 @@ class TestGoFZF < TestBase tmux.send_keys :Down tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip } end + + def test_header_first + tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first", :Enter + tmux.until do |lines| + expected = <<~OUTPUT + > 4 + 997/997 + > + 3 + 2 + 1 + foobar + OUTPUT + + assert_equal expected.chomp, lines.reverse.take(7).reverse.join("\n") + end + end + + def test_header_first_reverse + tmux.send_keys "seq 1000 | #{FZF} --header foobar --header-lines 3 --header-first --reverse --inline-info", :Enter + tmux.until do |lines| + expected = <<~OUTPUT + foobar + 1 + 2 + 3 + > < 997/997 + > 4 + OUTPUT + + assert_equal expected.chomp, lines.take(6).join("\n") + end + end end module TestShell