summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHiroki Konishi <relastle@gmail.com>2020-02-17 10:19:03 +0900
committerGitHub <noreply@github.com>2020-02-17 10:19:03 +0900
commit2a60edcd52df0c913ea8a93efec4027b9a758a5b (patch)
tree6ff74c8dd9996ad0f5a621c0175ccad18c2b55b4
parentd61ac32d7b5f1ba0c98c5fff3ec33f76c865e96e (diff)
Make pointer and multi-select marker customizable (#1844)
Add --pointer and --marker option which can provide additional context to the user
-rw-r--r--man/man1/fzf.16
-rw-r--r--shell/completion.bash2
-rw-r--r--src/options.go51
-rw-r--r--src/options_test.go26
-rw-r--r--src/terminal.go158
-rwxr-xr-xtest/test_go.rb33
6 files changed, 203 insertions, 73 deletions
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index d6fe163d..b7060af9 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -231,6 +231,12 @@ A synonym for \fB--info=hidden\fB
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.TP
+.BI "--pointer=" "STR"
+Pointer to the current line (default: '>')
+.TP
+.BI "--marker=" "STR"
+Multi-select marker (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--layout\fR option, and
diff --git a/shell/completion.bash b/shell/completion.bash
index 8b3025ac..9d52a6ba 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -72,6 +72,8 @@ _fzf_opts_completion() {
--margin
--inline-info
--prompt
+ --pointer
+ --marker
--header
--header-lines
--ansi
diff --git a/src/options.go b/src/options.go
index 815ac444..3bbeb1c5 100644
--- a/src/options.go
+++ b/src/options.go
@@ -6,12 +6,14 @@ import (
"regexp"
"strconv"
"strings"
+ "unicode"
"unicode/utf8"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
+ "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords"
)
@@ -59,6 +61,8 @@ const usage = `usage: fzf [options]
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden]
--prompt=STR Input prompt (default: '> ')
+ --pointer=STR Pointer to the current line (default: '>')
+ --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
@@ -189,6 +193,8 @@ type Options struct {
InfoStyle infoStyle
JumpLabels string
Prompt string
+ Pointer string
+ Marker string
Query string
Select1 bool
Exit0 bool
@@ -242,6 +248,8 @@ func defaultOptions() *Options {
InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
Prompt: "> ",
+ Pointer: ">",
+ Marker: ">",
Query: "",
Select1: false,
Exit0: false,
@@ -1041,6 +1049,8 @@ func parseOptions(opts *Options, allArgs []string) {
}
}
validateJumpLabels := false
+ validatePointer := false
+ validateMarker := false
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
switch arg {
@@ -1189,6 +1199,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false
case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required")
+ case "--pointer":
+ opts.Pointer = nextString(allArgs, &i, "pointer sign string required")
+ validatePointer = true
+ case "--marker":
+ opts.Marker = nextString(allArgs, &i, "selected sign string required")
+ validateMarker = true
case "--sync":
opts.Sync = true
case "--no-sync":
@@ -1255,6 +1271,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value
+ } else if match, value := optString(arg, "--pointer="); match {
+ opts.Pointer = value
+ validatePointer = true
+ } else if match, value := optString(arg, "--marker="); match {
+ opts.Marker = value
+ validateMarker = true
} else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match {
@@ -1333,6 +1355,35 @@ func parseOptions(opts *Options, allArgs []string) {
}
}
}
+
+ if validatePointer {
+ if err := validateSign(opts.Pointer, "pointer"); err != nil {
+ errorExit(err.Error())
+ }
+ }
+
+ if validateMarker {
+ if err := validateSign(opts.Marker, "marker"); err != nil {
+ errorExit(err.Error())
+ }
+ }
+}
+
+func validateSign(sign string, signOptName string) error {
+ if sign == "" {
+ return fmt.Errorf("%v cannot be empty", signOptName)
+ }
+ widthSum := 0
+ for _, r := range sign {
+ if !unicode.IsGraphic(r) {
+ return fmt.Errorf("invalid character in %v", signOptName)
+ }
+ widthSum += runewidth.RuneWidth(r)
+ if widthSum > 2 {
+ return fmt.Errorf("%v display width should be up to 2", signOptName)
+ }
+ }
+ return nil
}
func postProcessOptions(opts *Options) {
diff --git a/src/options_test.go b/src/options_test.go
index 66d7c8f5..b312be11 100644
--- a/src/options_test.go
+++ b/src/options_test.go
@@ -422,3 +422,29 @@ func TestAdditiveExpect(t *testing.T) {
t.Error(opts.Expect)
}
}
+
+func TestValidateSign(t *testing.T) {
+ testCases := []struct {
+ inputSign string
+ isValid bool
+ }{
+ {"> ", true},
+ {"아", true},
+ {"😀", true},
+ {"", false},
+ {">>>", false},
+ {"\n", false},
+ {"\t", false},
+ }
+
+ for _, testCase := range testCases {
+ err := validateSign(testCase.inputSign, "")
+ if testCase.isValid && err != nil {
+ t.Errorf("Input sign `%s` caused error", testCase.inputSign)
+ }
+
+ if !testCase.isValid && err == nil {
+ t.Errorf("Input sign `%s` did not cause error", testCase.inputSign)
+ }
+ }
+}
diff --git a/src/terminal.go b/src/terminal.go
index e9101a52..bc975546 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -59,72 +59,78 @@ var emptyLine = itemLine{}
// Terminal represents terminal input/output
type Terminal struct {
- initDelay time.Duration
- infoStyle infoStyle
- spinner []string
- prompt string
- promptLen int
- queryLen [2]int
- layout layoutType
- fullscreen bool
- hscroll bool
- hscrollOff int
- wordRubout string
- wordNext string
- cx int
- cy int
- offset int
- xoffset int
- yanked []rune
- input []rune
- multi int
- sort bool
- toggleSort bool
- delimiter Delimiter
- expect map[int]string
- keymap map[int][]action
- pressed string
- printQuery bool
- history *History
- cycle bool
- header []string
- header0 []string
- ansi bool
- tabstop int
- margin [4]sizeSpec
- strong tui.Attr
- unicode bool
- bordered bool
- cleanExit bool
- border tui.Window
- window tui.Window
- pborder tui.Window
- pwindow tui.Window
- count int
- progress int
- reading bool
- failed *string
- jumping jumpMode
- jumpLabels string
- printer func(string)
- printsep string
- merger *Merger
- selected map[int32]selectedItem
- version int64
- reqBox *util.EventBox
- preview previewOpts
- previewer previewer
- previewBox *util.EventBox
- eventBox *util.EventBox
- mutex sync.Mutex
- initFunc func()
- prevLines []itemLine
- suppress bool
- startChan chan bool
- killChan chan int
- slab *util.Slab
- theme *tui.ColorTheme
- tui tui.Renderer
+ initDelay time.Duration
+ infoStyle infoStyle
+ spinner []string
+ prompt string
+ promptLen int
+ pointer string
+ pointerLen int
+ pointerEmpty string
+ marker string
+ markerLen int
+ markerEmpty string
+ queryLen [2]int
+ layout layoutType
+ fullscreen bool
+ hscroll bool
+ hscrollOff int
+ wordRubout string
+ wordNext string
+ cx int
+ cy int
+ offset int
+ xoffset int
+ yanked []rune
+ input []rune
+ multi int
+ sort bool
+ toggleSort bool
+ delimiter Delimiter
+ expect map[int]string
+ keymap map[int][]action
+ pressed string
+ printQuery bool
+ history *History
+ cycle bool
+ header []string
+ header0 []string
+ ansi bool
+ tabstop int
+ margin [4]sizeSpec
+ strong tui.Attr
+ unicode bool
+ bordered bool
+ cleanExit bool
+ border tui.Window
+ window tui.Window
+ pborder tui.Window
+ pwindow tui.Window
+ count int
+ progress int
+ reading bool
+ failed *string
+ jumping jumpMode
+ jumpLabels string
+ printer func(string)
+ printsep string
+ merger *Merger
+ selected map[int32]selectedItem
+ version int64
+ reqBox *util.EventBox
+ preview previewOpts
+ previewer previewer
+ previewBox *util.EventBox
+ eventBox *util.EventBox
+ mutex sync.Mutex
+ initFunc func()
+ prevLines []itemLine
+ suppress bool
+ startChan chan bool
+ killChan chan int
+ slab *util.Slab
+ theme *tui.ColorTheme
+ tui tui.Renderer
}
type selectedItem struct {
@@ -441,6 +447,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
tui: renderer,
initFunc: func() { renderer.Init() }}
t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0)
+ t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
+ t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
+ // Pre-calculated empty pointer and marker signs
+ t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
+ t.markerEmpty = strings.Repeat(" ", t.markerLen)
+
return &t
}
@@ -852,15 +864,15 @@ func (t *Terminal) printList() {
func (t *Terminal) printItem(result Result, line int, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()]
- label := " "
+ label := t.pointerEmpty
if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) {
// Striped
current = i%2 == 0
- label = t.jumpLabels[i : i+1]
+ label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
}
} else if current {
- label = ">"
+ label = t.pointer
}
// Avoid unnecessary redraw
@@ -879,17 +891,17 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
if current {
t.window.CPrint(tui.ColCurrentCursor, t.strong, label)
if selected {
- t.window.CPrint(tui.ColCurrentSelected, t.strong, ">")
+ t.window.CPrint(tui.ColCurrentSelected, t.strong, t.marker)
} else {
- t.window.CPrint(tui.ColCurrentSelected, t.strong, " ")
+ t.window.CPrint(tui.ColCurrentSelected, t.strong, t.markerEmpty)
}
newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
} else {
t.window.CPrint(tui.ColCursor, t.strong, label)
if selected {
- t.window.CPrint(tui.ColSelected, t.strong, ">")
+ t.window.CPrint(tui.ColSelected, t.strong, t.marker)
} else {
- t.window.Print(" ")
+ t.window.Print(t.markerEmpty)
}
newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true)
}
diff --git a/test/test_go.rb b/test/test_go.rb
index d2fe1809..22862d7d 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1407,6 +1407,39 @@ class TestGoFZF < TestBase
assert_equal '3', readonce.chomp
end
+ def test_pointer
+ pointer = '>>'
+ tmux.send_keys "seq 10 | #{fzf "--pointer '#{pointer}'"}", :Enter
+ tmux.until { |lines| lines[-2] == ' 10/10' }
+ lines = tmux.capture
+ # Assert that specified pointer is displayed
+ assert_equal "#{pointer} 1", lines[-3]
+ end
+
+ def test_pointer_with_jump
+ pointer = '>>'
+ tmux.send_keys "seq 10 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump' --pointer '#{pointer}'"}", :Enter
+ tmux.until { |lines| lines[-2] == ' 10/10' }
+ tmux.send_keys 'C-j'
+ # Correctly padded jump label should appear
+ tmux.until { |lines| lines[-7] == '5 5' }
+ tmux.until { |lines| lines[-8] == ' 6' }
+ tmux.send_keys '5'
+ lines = tmux.capture
+ # Assert that specified pointer is displayed
+ assert_equal "#{pointer} 5", lines[-7]
+ end
+
+ def test_marker
+ marker = '>>'
+ tmux.send_keys "seq 10 | #{fzf "--multi --marker '#{marker}'"}", :Enter
+ tmux.until { |lines| lines[-2] == ' 10/10' }
+ tmux.send_keys :BTab
+ lines = tmux.capture
+ # Assert that specified marker is displayed
+ assert_equal " #{marker}1", lines[-3]
+ end
+
def test_preview
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter
tmux.until { |lines| lines[1].include?(' {1-1}') }