From daa1958f8602f91f6df6dac7a87c93da53aed5e4 Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Sun, 10 Jun 2018 01:41:50 +0900 Subject: [PATCH] Provide an option to reverse items only (#1267) --- README.md | 8 ++++---- man/man1/fzf.1 | 21 +++++++++++++++++---- src/options.go | 40 +++++++++++++++++++++++++++++++++------ src/terminal.go | 50 +++++++++++++++++++++++++++++++++++-------------- test/test_go.rb | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 136 insertions(+), 30 deletions(-) mode change 100644 => 100755 test/test_go.rb diff --git a/README.md b/README.md index e6a6dcaf..30578bac 100644 --- a/README.md +++ b/README.md @@ -223,8 +223,8 @@ cursor with `--height` option. vim $(fzf --height 40%) ``` -Also check out `--reverse` option if you prefer "top-down" layout instead of -the default "bottom-up" layout. +Also check out `--reverse` and `--layout` options if you prefer +"top-down" layout instead of the default "bottom-up" layout. ```sh vim $(fzf --height 40% --reverse) @@ -234,7 +234,7 @@ You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by default. For example, ```sh -export FZF_DEFAULT_OPTS='--height 40% --reverse --border' +export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' ``` #### Search syntax @@ -272,7 +272,7 @@ or `py`. - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'` - `FZF_DEFAULT_OPTS` - Default options - - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` + - e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"` #### Options diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5dd00e49..b113be41 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -155,9 +155,22 @@ the full screen. .BI "--min-height=" "HEIGHT" Minimum height when \fB--height\fR is given in percent (default: 10). Ignored when \fB--height\fR is not specified. +.TP +.BI "--layout=" "LAYOUT" +Choose the layout (default: default) + +.br +.BR default " Display from the bottom of the screen" +.br +.BR reverse " Display from the top of the screen" +.br +.BR reverse-list " Display from the top of the screen, prompt at the bottom" +.br + .TP .B "--reverse" -Reverse orientation +A synonym for \fB--layout=reverse\fB + .TP .B "--border" Draw border above and below the finder @@ -195,7 +208,7 @@ Input prompt (default: '> ') .TP .BI "--header=" "STR" The given string will be printed as the sticky header. The lines are displayed -in the given order from top to bottom regardless of \fB--reverse\fR option, and +in the given order from top to bottom regardless of \fB--layout\fR option, and are not affected by \fB--with-nth\fR. ANSI color codes are processed even when \fB--ansi\fR is not set. .TP @@ -543,8 +556,8 @@ triggered whenever the query string is changed. \fBtoggle\fR (\fIright-click\fR) \fBtoggle-all\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR - \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) - \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) + \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) + \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR \fBtoggle-preview-wrap\fR \fBtoggle-sort\fR diff --git a/src/options.go b/src/options.go index 45eca5ef..fc32344c 100644 --- a/src/options.go +++ b/src/options.go @@ -53,7 +53,7 @@ const usage = `usage: fzf [options] height instead of using fullscreen --min-height=HEIGHT Minimum height when --height is given in percent (default: 10) - --reverse Reverse orientation + --layout=LAYOUT Choose layout: [default|reverse|reverse-list] --border Draw border above and below the finder --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query @@ -90,7 +90,8 @@ const usage = `usage: fzf [options] Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Default options (e.g. '--reverse --inline-info') + FZF_DEFAULT_OPTS Default options + (e.g. '--layout=reverse --inline-info') ` @@ -132,6 +133,14 @@ const ( posRight ) +type layoutType int + +const ( + layoutDefault layoutType = iota + layoutReverse + layoutReverseList +) + type previewOpts struct { command string position windowPosition @@ -161,7 +170,7 @@ type Options struct { Bold bool Height sizeSpec MinHeight int - Reverse bool + Layout layoutType Cycle bool Hscroll bool HscrollOff int @@ -211,7 +220,7 @@ func defaultOptions() *Options { Black: false, Bold: true, MinHeight: 10, - Reverse: false, + Layout: layoutDefault, Cycle: false, Hscroll: true, HscrollOff: 10, @@ -857,6 +866,20 @@ func parseHeight(str string) sizeSpec { return size } +func parseLayout(str string) layoutType { + switch str { + case "default": + return layoutDefault + case "reverse": + return layoutReverse + case "reverse-list": + return layoutReverseList + default: + errorExit("invalid layout (expected: default / reverse / reverse-list)") + } + return layoutDefault +} + func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight @@ -1037,10 +1060,13 @@ func parseOptions(opts *Options, allArgs []string) { opts.Bold = true case "--no-bold": opts.Bold = false + case "--layout": + opts.Layout = parseLayout( + nextString(allArgs, &i, "layout required (default / reverse / reverse-list)")) case "--reverse": - opts.Reverse = true + opts.Layout = layoutReverse case "--no-reverse": - opts.Reverse = false + opts.Layout = layoutDefault case "--cycle": opts.Cycle = true case "--no-cycle": @@ -1156,6 +1182,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Height = parseHeight(value) } else if match, value := optString(arg, "--min-height="); match { opts.MinHeight = atoi(value) + } else if match, value := optString(arg, "--layout="); match { + opts.Layout = parseLayout(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/terminal.go b/src/terminal.go index 664b7d47..d4685042 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -59,7 +59,7 @@ type Terminal struct { inlineInfo bool prompt string promptLen int - reverse bool + layout layoutType fullscreen bool hscroll bool hscrollOff int @@ -302,10 +302,11 @@ func trimQuery(query string) []rune { func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := trimQuery(opts.Query) var header []string - if opts.Reverse { - header = opts.Header - } else { + switch opts.Layout { + case layoutDefault, layoutReverseList: header = reverseStringArray(opts.Header) + default: + header = opts.Header } var delay time.Duration if opts.Tac { @@ -363,7 +364,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { t := Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, - reverse: opts.Reverse, + layout: opts.Layout, fullscreen: fullscreen, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, @@ -643,8 +644,21 @@ func (t *Terminal) resizeWindows() { } func (t *Terminal) move(y int, x int, clear bool) { - if !t.reverse { - y = t.window.Height() - y - 1 + h := t.window.Height() + + switch t.layout { + case layoutDefault: + y = h - y - 1 + case layoutReverseList: + n := 2 + len(t.header) + if t.inlineInfo { + n-- + } + if y < n { + y = h - y - 1 + } else { + y -= n + } } if clear { @@ -748,7 +762,7 @@ func (t *Terminal) printList() { count := t.merger.Length() - t.offset for j := 0; j < maxy; j++ { i := j - if !t.reverse { + if t.layout == layoutDefault { i = maxy - 1 - j } line := i + 2 + len(t.header) @@ -1680,12 +1694,12 @@ func (t *Terminal) Loop() { req(reqList, reqInfo) } case actToggleIn: - if t.reverse { + if t.layout != layoutDefault { return doAction(action{t: actToggleUp}, mapkey) } return doAction(action{t: actToggleDown}, mapkey) case actToggleOut: - if t.reverse { + if t.layout != layoutDefault { return doAction(action{t: actToggleDown}, mapkey) } return doAction(action{t: actToggleUp}, mapkey) @@ -1813,13 +1827,21 @@ func (t *Terminal) Loop() { mx -= t.window.Left() my -= t.window.Top() mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) - if !t.reverse { - my = t.window.Height() - my - 1 - } min := 2 + len(t.header) if t.inlineInfo { min-- } + h := t.window.Height() + switch t.layout { + case layoutDefault: + my = h - my - 1 + case layoutReverseList: + if my < h-min { + my += min + } else { + my = h - my - 1 + } + } if me.Double { // Double-click if my >= min { @@ -1912,7 +1934,7 @@ func (t *Terminal) constrain() { } func (t *Terminal) vmove(o int, allowCycle bool) { - if t.reverse { + if t.layout != layoutDefault { o *= -1 } dest := t.cy + o diff --git a/test/test_go.rb b/test/test_go.rb old mode 100644 new mode 100755 index 4f4f94e1..c7758068 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1060,6 +1060,21 @@ class TestGoFZF < TestBase assert_equal '50', readonce.chomp end + def test_header_lines_reverse_list + tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --layout=reverse-list'}", :Enter + 2.times do + tmux.until do |lines| + lines[0] == '> 50' && + lines[-4] == ' 2' && + lines[-3] == ' 1' && + lines[-2].include?('/90') + end + tmux.send_keys :Up + end + tmux.send_keys :Enter + assert_equal '50', readonce.chomp + end + def test_header_lines_overflow tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter tmux.until do |lines| @@ -1087,7 +1102,8 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('100/100') && - lines[-7..-3].map(&:strip) == header + lines[-7..-3].map(&:strip) == header && + lines[-8] == '> 1' end end @@ -1096,7 +1112,18 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('100/100') && - lines[2..6].map(&:strip) == header + lines[2..6].map(&:strip) == header && + lines[7] == '> 1' + end + end + + def test_header_reverse_list + tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{FILE})\\\" --layout=reverse-list"}", :Enter + header = File.readlines(FILE).take(5).map(&:strip) + tmux.until do |lines| + lines[-2].include?('100/100') && + lines[-7..-3].map(&:strip) == header && + lines[0] == '> 1' end end @@ -1120,6 +1147,16 @@ class TestGoFZF < TestBase end end + def test_header_and_header_lines_reverse_list + tmux.send_keys "seq 100 | #{fzf "--layout=reverse-list --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter + header = File.readlines(FILE).take(5).map(&:strip) + tmux.until do |lines| + lines[-2].include?('90/90') && + lines[-7...-2].map(&:strip) == header && + lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse + end + end + def test_cancel tmux.send_keys "seq 10 | #{fzf '--bind 2:cancel'}", :Enter tmux.until { |lines| lines[-2].include?('10/10') } @@ -1145,6 +1182,12 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_margin_reverse_list + tmux.send_keys "yes | head -1000 | #{fzf '--margin 5,3 --layout=reverse-list'}", :Enter + tmux.until { |lines| lines[4] == '' && lines[5] == ' > y' } + tmux.send_keys :Enter + end + def test_tabstop writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"] {