Add scrollbar

Close #3096
pull/3110/head
Junegunn Choi 1 year ago
parent ec20dfe312
commit 5cd6f1d064
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

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

@ -356,6 +356,15 @@ ANSI color codes are supported.
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: '> ')

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

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

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

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

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

Loading…
Cancel
Save