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 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) - 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 - You can set it to a very large number so that the cursor stays in the
middle of the screen while scrolling 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. 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 .SH NAME
fzf - a command-line fuzzy finder 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 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 \fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow. lines that follow.
.TP
.B "--header-first"
Print header before the prompt line
.SS Display .SS Display
.TP .TP
.B "--ansi" .B "--ansi"

View File

@ -69,6 +69,7 @@ const usage = `usage: fzf [options]
--marker=STR Multi-select marker (default: '>') --marker=STR Multi-select marker (default: '>')
--header=STR String to print as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated 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 Display
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
@ -225,6 +226,7 @@ type Options struct {
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
HeaderFirst bool
Margin [4]sizeSpec Margin [4]sizeSpec
Padding [4]sizeSpec Padding [4]sizeSpec
BorderShape tui.BorderShape BorderShape tui.BorderShape
@ -287,6 +289,7 @@ func defaultOptions() *Options {
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
HeaderFirst: false,
Margin: defaultMargin(), Margin: defaultMargin(),
Padding: defaultMargin(), Padding: defaultMargin(),
Unicode: true, Unicode: true,
@ -1427,6 +1430,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--header-lines": case "--header-lines":
opts.HeaderLines = atoi( opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required")) nextString(allArgs, &i, "number of header lines required"))
case "--header-first":
opts.HeaderFirst = true
case "--no-header-first":
opts.HeaderFirst = false
case "--preview": case "--preview":
opts.Preview.command = nextString(allArgs, &i, "preview command required") opts.Preview.command = nextString(allArgs, &i, "preview command required")
case "--no-preview": case "--no-preview":

View File

@ -140,6 +140,8 @@ type Terminal struct {
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
headerFirst bool
headerLines int
header []string header []string
header0 []string header0 []string
ansi bool ansi bool
@ -529,6 +531,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
paused: opts.Phony, paused: opts.Phony,
strong: strongAttr, strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
header: header, header: header,
header0: header, header0: header,
ansi: opts.Ansi, ansi: opts.Ansi,
@ -976,12 +980,23 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
return before, after 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() { 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() { func (t *Terminal) printPrompt() {
t.move(0, 0, true) t.move(t.promptLine(), 0, true)
t.prompt() t.prompt()
before, after := t.updatePromptOffset() before, after := t.updatePromptOffset()
@ -1003,22 +1018,23 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
pos := 0 pos := 0
line := t.promptLine()
switch t.infoStyle { switch t.infoStyle {
case infoDefault: case infoDefault:
t.move(1, 0, true) t.move(line+1, 0, true)
if t.reading { if t.reading {
duration := int64(spinnerDuration) duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration idx := (time.Now().UnixNano() % (duration * int64(len(t.spinner)))) / duration
t.window.CPrint(tui.ColSpinner, t.spinner[idx]) t.window.CPrint(tui.ColSpinner, t.spinner[idx])
} }
t.move(1, 2, false) t.move(line+1, 2, false)
pos = 2 pos = 2
case infoInline: case infoInline:
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
if pos+len(" < ") > t.window.Width() { if pos+len(" < ") > t.window.Width() {
return return
} }
t.move(0, pos, true) t.move(line, pos, true)
if t.reading { if t.reading {
t.window.CPrint(tui.ColSpinner, " < ") t.window.CPrint(tui.ColSpinner, " < ")
} else { } else {
@ -1061,11 +1077,20 @@ func (t *Terminal) printHeader() {
return return
} }
max := t.window.Height() max := t.window.Height()
if t.headerFirst {
max--
if !t.noInfoLine() {
max--
}
}
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
line := idx + 2 line := idx
if t.noInfoLine() { if !t.headerFirst {
line-- line++
if !t.noInfoLine() {
line++
}
} }
if line >= max { if line >= max {
continue continue
@ -2644,7 +2669,7 @@ func (t *Terminal) Loop() {
} }
} }
} else if me.Down { } else if me.Down {
if my == 0 && mx >= 0 { if my == t.promptLine() && mx >= 0 {
// Prompt // Prompt
t.cx = mx + t.xoffset t.cx = mx + t.xoffset
} else if my >= min { } else if my >= min {

View File

@ -2109,6 +2109,39 @@ class TestGoFZF < TestBase
tmux.send_keys :Down tmux.send_keys :Down
tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip } tmux.until { |lines| assert_equal "> #{height + 1}", lines[height / 2].strip }
end 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 end
module TestShell module TestShell