m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 14:23:47 -05:00

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
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" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
.TP .TP
.BI "--pointer=" "STR"
Pointer to the current line (default: '>')
.TP
.BI "--marker=" "STR"
Multi-select marker (default: '>')
.TP
.BI "--header=" "STR" .BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed 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 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 --margin
--inline-info --inline-info
--prompt --prompt
--pointer
--marker
--header --header
--header-lines --header-lines
--ansi --ansi

View File

@@ -6,12 +6,14 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords" "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) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden] --info=STYLE Finder info style [default|inline|hidden]
--prompt=STR Input prompt (default: '> ') --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=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
@@ -189,6 +193,8 @@ type Options struct {
InfoStyle infoStyle InfoStyle infoStyle
JumpLabels string JumpLabels string
Prompt string Prompt string
Pointer string
Marker string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
@@ -242,6 +248,8 @@ func defaultOptions() *Options {
InfoStyle: infoDefault, InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Pointer: ">",
Marker: ">",
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
@@ -1041,6 +1049,8 @@ func parseOptions(opts *Options, allArgs []string) {
} }
} }
validateJumpLabels := false validateJumpLabels := false
validatePointer := false
validateMarker := false
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -1189,6 +1199,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false opts.PrintQuery = false
case "--prompt": case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required") 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": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync": case "--no-sync":
@@ -1255,6 +1271,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Delimiter = delimiterRegexp(value) opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value 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 { } else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value) opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match { } 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) { func postProcessOptions(opts *Options) {

View File

@@ -422,3 +422,29 @@ func TestAdditiveExpect(t *testing.T) {
t.Error(opts.Expect) 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 // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoStyle infoStyle infoStyle infoStyle
spinner []string spinner []string
prompt string prompt string
promptLen int promptLen int
queryLen [2]int pointer string
layout layoutType pointerLen int
fullscreen bool pointerEmpty string
hscroll bool marker string
hscrollOff int markerLen int
wordRubout string markerEmpty string
wordNext string queryLen [2]int
cx int layout layoutType
cy int fullscreen bool
offset int hscroll bool
xoffset int hscrollOff int
yanked []rune wordRubout string
input []rune wordNext string
multi int cx int
sort bool cy int
toggleSort bool offset int
delimiter Delimiter xoffset int
expect map[int]string yanked []rune
keymap map[int][]action input []rune
pressed string multi int
printQuery bool sort bool
history *History toggleSort bool
cycle bool delimiter Delimiter
header []string expect map[int]string
header0 []string keymap map[int][]action
ansi bool pressed string
tabstop int printQuery bool
margin [4]sizeSpec history *History
strong tui.Attr cycle bool
unicode bool header []string
bordered bool header0 []string
cleanExit bool ansi bool
border tui.Window tabstop int
window tui.Window margin [4]sizeSpec
pborder tui.Window strong tui.Attr
pwindow tui.Window unicode bool
count int bordered bool
progress int cleanExit bool
reading bool border tui.Window
failed *string window tui.Window
jumping jumpMode pborder tui.Window
jumpLabels string pwindow tui.Window
printer func(string) count int
printsep string progress int
merger *Merger reading bool
selected map[int32]selectedItem failed *string
version int64 jumping jumpMode
reqBox *util.EventBox jumpLabels string
preview previewOpts printer func(string)
previewer previewer printsep string
previewBox *util.EventBox merger *Merger
eventBox *util.EventBox selected map[int32]selectedItem
mutex sync.Mutex version int64
initFunc func() reqBox *util.EventBox
prevLines []itemLine preview previewOpts
suppress bool previewer previewer
startChan chan bool previewBox *util.EventBox
killChan chan int eventBox *util.EventBox
slab *util.Slab mutex sync.Mutex
theme *tui.ColorTheme initFunc func()
tui tui.Renderer prevLines []itemLine
suppress bool
startChan chan bool
killChan chan int
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
} }
type selectedItem struct { type selectedItem struct {
@@ -441,6 +447,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
tui: renderer, tui: renderer,
initFunc: func() { renderer.Init() }} initFunc: func() { renderer.Init() }}
t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0) 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 return &t
} }
@@ -852,15 +864,15 @@ func (t *Terminal) printList() {
func (t *Terminal) printItem(result Result, line int, i int, current bool) { func (t *Terminal) printItem(result Result, line int, i int, current bool) {
item := result.item item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " " label := t.pointerEmpty
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
if i < len(t.jumpLabels) { if i < len(t.jumpLabels) {
// Striped // Striped
current = i%2 == 0 current = i%2 == 0
label = t.jumpLabels[i : i+1] label = t.jumpLabels[i:i+1] + strings.Repeat(" ", t.pointerLen-1)
} }
} else if current { } else if current {
label = ">" label = t.pointer
} }
// Avoid unnecessary redraw // Avoid unnecessary redraw
@@ -879,17 +891,17 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
if current { if current {
t.window.CPrint(tui.ColCurrentCursor, t.strong, label) t.window.CPrint(tui.ColCurrentCursor, t.strong, label)
if selected { if selected {
t.window.CPrint(tui.ColCurrentSelected, t.strong, ">") t.window.CPrint(tui.ColCurrentSelected, t.strong, t.marker)
} else { } 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) newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
} else { } else {
t.window.CPrint(tui.ColCursor, t.strong, label) t.window.CPrint(tui.ColCursor, t.strong, label)
if selected { if selected {
t.window.CPrint(tui.ColSelected, t.strong, ">") t.window.CPrint(tui.ColSelected, t.strong, t.marker)
} else { } else {
t.window.Print(" ") t.window.Print(t.markerEmpty)
} }
newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) 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 assert_equal '3', readonce.chomp
end 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 def test_preview
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter 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}') } tmux.until { |lines| lines[1].include?(' {1-1}') }