summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-01-01 14:48:14 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-01-01 14:48:14 +0900
commit5cd6f1d06427f1e023573be084e384227fae3cdf (patch)
tree912b9359e1767e4de768df65206547eedfac729f
parentec20dfe312fb31dca2ebf7aa0caa05cd582cf131 (diff)
Add scrollbar
Close #3096
-rw-r--r--CHANGELOG.md8
-rw-r--r--man/man1/fzf.19
-rw-r--r--src/options.go23
-rw-r--r--src/terminal.go63
-rw-r--r--src/tui/light.go5
-rw-r--r--src/tui/tui.go9
-rwxr-xr-xtest/test_go.rb8
7 files changed, 116 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6126451..d2838d50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,14 @@ CHANGELOG
# Send actions to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
```
+- Added scrollbar on the main search window
+ ```sh
+ # Hide scrollbar
+ fzf --no-scrollbar
+
+ # Customize scrollbar
+ fzf --scrollbar ┆ --color scrollbar:blue
+ ```
- New event
- Added `load` event that is triggered when the input stream is complete
and the initial processing of the list is complete.
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index e981a13c..9ac213cd 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -357,6 +357,15 @@ Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB
.TP
+.BI "--scrollbar=" "CHAR"
+Use the given character to render scrollbar. (default: '▏' or ':' depending on
+\fB--no-unicode\fR).
+
+.TP
+.B "--no-scrollbar"
+Do not display scrollbar. A synonym for \fB--scrollbar=''\fB
+
+.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.TP
diff --git a/src/options.go b/src/options.go
index 0ee7db33..e4a6aa39 100644
--- a/src/options.go
+++ b/src/options.go
@@ -73,6 +73,8 @@ const usage = `usage: fzf [options]
--info=STYLE Finder info style [default|inline|hidden]
--separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator
+ --scrollbar[=CHAR] Scrollbar character
+ --no-scrollbar Hide scrollbar
--prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>')
--marker=STR Multi-select marker (default: '>')
@@ -290,6 +292,7 @@ type Options struct {
HeaderLines int
HeaderFirst bool
Ellipsis string
+ Scrollbar *string
Margin [4]sizeSpec
Padding [4]sizeSpec
BorderShape tui.BorderShape
@@ -359,6 +362,7 @@ func defaultOptions() *Options {
HeaderLines: 0,
HeaderFirst: false,
Ellipsis: "..",
+ Scrollbar: nil,
Margin: defaultMargin(),
Padding: defaultMargin(),
Unicode: true,
@@ -847,6 +851,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
mergeAttr(&theme.Border)
case "separator":
mergeAttr(&theme.Separator)
+ case "scrollbar":
+ mergeAttr(&theme.Scrollbar)
case "label":
mergeAttr(&theme.BorderLabel)
case "preview-label":
@@ -1570,6 +1576,16 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-separator":
nosep := ""
opts.Separator = &nosep
+ case "--scrollbar":
+ given, bar := optionalNextString(allArgs, &i)
+ if given {
+ opts.Scrollbar = &bar
+ } else {
+ opts.Scrollbar = nil
+ }
+ case "--no-scrollbar":
+ noBar := ""
+ opts.Scrollbar = &noBar
case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true
@@ -1739,6 +1755,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value
+ } else if match, value := optString(arg, "--scrollbar="); match {
+ opts.Scrollbar = &value
} else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match {
@@ -1845,6 +1863,11 @@ func postProcessOptions(opts *Options) {
if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform")
}
+
+ if opts.Scrollbar != nil && runewidth.StringWidth(*opts.Scrollbar) > 1 {
+ errorExit("scrollbar display width should be 1")
+ }
+
// Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs {
diff --git a/src/terminal.go b/src/terminal.go
index a89b6ac8..ed585e9d 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -97,6 +97,7 @@ type itemLine struct {
label string
queryLen int
width int
+ bar bool
result Result
}
@@ -161,6 +162,7 @@ type Terminal struct {
header []string
header0 []string
ellipsis string
+ scrollbar string
ansi bool
tabstop int
margin [4]sizeSpec
@@ -632,6 +634,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
+ if opts.Scrollbar == nil {
+ if t.unicode {
+ t.scrollbar = "▏" // Left one eighth block
+ } else {
+ t.scrollbar = "|"
+ }
+ } else {
+ t.scrollbar = *opts.Scrollbar
+ }
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
@@ -763,6 +774,27 @@ func (t *Terminal) noInfoLine() bool {
return t.infoStyle != infoDefault
}
+func (t *Terminal) getScrollbar() (int, int) {
+ total := t.merger.Length()
+ if total == 0 {
+ return 0, 0
+ }
+
+ maxItems := t.maxItems()
+ barLength := util.Max(1, maxItems*maxItems/total)
+ if total <= maxItems {
+ return 0, 0
+ }
+
+ var barStart int
+ if total == maxItems {
+ barStart = 0
+ } else {
+ barStart = (maxItems - barLength) * t.offset / (total - maxItems)
+ }
+ return barLength, barStart
+}
+
// Input returns current query string
func (t *Terminal) Input() (bool, []rune) {
t.mutex.Lock()
@@ -1349,6 +1381,7 @@ func (t *Terminal) printHeader() {
func (t *Terminal) printList() {
t.constrain()
+ barLength, barStart := t.getScrollbar()
maxy := t.maxItems()
count := t.merger.Length() - t.offset
@@ -1362,7 +1395,7 @@ func (t *Terminal) printList() {
line--
}
if i < count {
- t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset)
+ t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset, i >= barStart && i < barStart+barLength)
} else if t.prevLines[i] != emptyLine {
t.prevLines[i] = emptyLine
t.move(line, 0, true)
@@ -1370,7 +1403,7 @@ 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, bar bool) {
item := result.item
_, selected := t.selected[item.Index()]
label := ""
@@ -1386,7 +1419,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
// Avoid unnecessary redraw
newLine := itemLine{current: current, selected: selected, label: label,
- result: result, queryLen: len(t.input), width: 0}
+ result: result, queryLen: len(t.input), width: 0, bar: bar}
prevLine := t.prevLines[i]
if prevLine.current == newLine.current &&
prevLine.selected == newLine.selected &&
@@ -1426,6 +1459,12 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) {
if fillSpaces > 0 {
t.window.Print(strings.Repeat(" ", fillSpaces))
}
+ if len(t.scrollbar) > 0 && bar != prevLine.bar {
+ t.move(line, t.window.Width()-1, true)
+ if bar {
+ t.window.CPrint(tui.ColScrollbar, t.scrollbar)
+ }
+ }
t.prevLines[i] = newLine
}
@@ -2999,8 +3038,9 @@ func (t *Terminal) Loop() {
}
} else if t.window.Enclose(my, mx) {
mx -= t.window.Left()
- my -= t.window.Top()
+ bar := mx == t.window.Width()-1
mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
+ my -= t.window.Top()
min := 2 + len(t.header)
if t.noInfoLine() {
min--
@@ -3016,7 +3056,20 @@ func (t *Terminal) Loop() {
my = h - my - 1
}
}
- if me.Double {
+ if bar && my >= min {
+ barLength, barStart := t.getScrollbar()
+ if barLength > 0 {
+ maxItems := t.maxItems()
+ if newBarStart := util.Constrain(my-min-barLength/2, 0, maxItems-barLength); newBarStart != barStart {
+ total := t.merger.Length()
+ prevOffset := t.offset
+ // barStart = (maxItems - barLength) * t.offset / (total - maxItems)
+ t.offset = int(math.Ceil(float64(newBarStart) * float64(total-maxItems) / float64(maxItems-barLength)))
+ t.cy = t.offset + t.cy - prevOffset
+ req(reqList)
+ }
+ }
+ } else if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
diff --git a/src/tui/light.go b/src/tui/light.go
index 4225fc52..83020a71 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -176,6 +176,7 @@ func (r *LightRenderer) Init() {
if r.mouse {
r.csi("?1000h")
+ r.csi("?1002h")
r.csi("?1006h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
@@ -569,12 +570,14 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
// ctrl := t & 0b1000
mod := t&0b1100 > 0
+ drag := t&0b100000 > 0
+
if scroll != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
}
double := false
- if down {
+ if down && !drag {
now := time.Now()
if !left { // Right double click is not allowed
r.clickY = []int{}
diff --git a/src/tui/tui.go b/src/tui/tui.go
index d1bb571a..e0fc6330 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -269,6 +269,7 @@ type ColorTheme struct {
Selected ColorAttr
Header ColorAttr
Separator ColorAttr
+ Scrollbar ColorAttr
Border ColorAttr
BorderLabel ColorAttr
PreviewLabel ColorAttr
@@ -466,6 +467,7 @@ var (
ColInfo ColorPair
ColHeader ColorPair
ColSeparator ColorPair
+ ColScrollbar ColorPair
ColBorder ColorPair
ColPreview ColorPair
ColPreviewBorder ColorPair
@@ -490,6 +492,7 @@ func EmptyTheme() *ColorTheme {
Selected: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Separator: ColorAttr{colUndefined, AttrUndefined},
+ Scrollbar: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
@@ -517,6 +520,7 @@ func NoColorTheme() *ColorTheme {
Selected: ColorAttr{colDefault, AttrRegular},
Header: ColorAttr{colDefault, AttrRegular},
Separator: ColorAttr{colDefault, AttrRegular},
+ Scrollbar: ColorAttr{colDefault, AttrRegular},
Border: ColorAttr{colDefault, AttrRegular},
BorderLabel: ColorAttr{colDefault, AttrRegular},
Disabled: ColorAttr{colDefault, AttrRegular},
@@ -549,6 +553,7 @@ func init() {
Selected: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Separator: ColorAttr{colBlack, AttrUndefined},
+ Scrollbar: ColorAttr{colBlack, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
@@ -573,6 +578,7 @@ func init() {
Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Separator: ColorAttr{59, AttrUndefined},
+ Scrollbar: ColorAttr{59, AttrUndefined},
Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
@@ -597,6 +603,7 @@ func init() {
Selected: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Separator: ColorAttr{145, AttrUndefined},
+ Scrollbar: ColorAttr{145, AttrUndefined},
Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined},
Disabled: ColorAttr{colUndefined, AttrUndefined},
@@ -645,6 +652,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
+ theme.Scrollbar = o(theme.Separator, theme.Scrollbar)
initPalette(theme)
}
@@ -677,6 +685,7 @@ func initPalette(theme *ColorTheme) {
ColInfo = pair(theme.Info, theme.Bg)
ColHeader = pair(theme.Header, theme.Bg)
ColSeparator = pair(theme.Separator, theme.Bg)
+ ColScrollbar = pair(theme.Scrollbar, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg)
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
diff --git a/test/test_go.rb b/test/test_go.rb
index 45c661a7..68caa352 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -23,7 +23,7 @@ DEFAULT_TIMEOUT = 10
FILE = File.expand_path(__FILE__)
BASE = File.expand_path('..', __dir__)
Dir.chdir(BASE)
-FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
+FZF = "FZF_DEFAULT_OPTS=--no-scrollbar FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf"
def wait
since = Time.now
@@ -65,7 +65,7 @@ class Shell
end
def fish
- UNSETS.map { |v| v + '= ' }.join + 'fish'
+ UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish'
end
end
end
@@ -2908,7 +2908,7 @@ class TestFish < TestBase
end
def new_shell
- tmux.send_keys 'env FZF_TMUX=1 fish', :Enter
+ tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| assert_empty lines }
end
@@ -2927,6 +2927,8 @@ unset <%= UNSETS.join(' ') %>
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
+export FZF_DEFAULT_OPTS=--no-scrollbar
+
# Setup fzf
# ---------
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then