Add --header-first option to display header before prompt line

Close #2422
This commit is contained in:
Junegunn Choi 2021-11-03 21:19:22 +09:00
parent ffd8bef808
commit 7bff4661f6
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 83 additions and 11 deletions

View File

@ -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

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
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"

View File

@ -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":

View File

@ -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 {

View File

@ -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