Make pointer and multi-select marker customizable (#1844)

Add --pointer and --marker option which can provide additional context to the user
This commit is contained in:
Hiroki Konishi 2020-02-17 10:19:03 +09:00 committed by GitHub
parent d61ac32d7b
commit 2a60edcd52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 203 additions and 73 deletions

View File

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

View File

@ -72,6 +72,8 @@ _fzf_opts_completion() {
--margin
--inline-info
--prompt
--pointer
--marker
--header
--header-lines
--ansi

View File

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

View File

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

View File

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

View File

@ -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}') }