m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 06:13: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

@@ -64,6 +64,12 @@ type Terminal struct {
spinner []string spinner []string
prompt string prompt string
promptLen int promptLen int
pointer string
pointerLen int
pointerEmpty string
marker string
markerLen int
markerEmpty string
queryLen [2]int queryLen [2]int
layout layoutType layout layoutType
fullscreen bool fullscreen bool
@@ -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}') }