Add --preview-wrap-sign

This commit is contained in:
Junegunn Choi
2026-02-19 21:29:03 +09:00
parent 0ecbf3f475
commit 4a195e6323
6 changed files with 269 additions and 202 deletions

View File

@@ -32,6 +32,8 @@ CHANGELOG
# In the preview window
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'"
```
- Added `--preview-wrap-sign` to set a different wrap indicator for the preview
window
- Added `alt-gutter` color option (#4602) (@hedgieinsocks)
- Added fish completion support (#4605) (@lalvarezt)
- zsh: Handle multi-line history selection (#4595) (@LangLangBart)

View File

@@ -919,6 +919,11 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.B * border\-bottom
.br
.TP
.BI "\-\-preview\-wrap\-sign" =INDICATOR
Indicator for wrapped lines in the preview window. If not set, the value of
\fB\-\-wrap\-sign\fR is used.
.TP
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
Position of the border label on the border line of the preview window. Specify

View File

@@ -219,6 +219,7 @@ _fzf_opts_completion() {
--with-shell
--wrap
--wrap-sign
--preview-wrap-sign
--zsh
-0 --exit-0
-1 --select-1

View File

@@ -167,6 +167,7 @@ Usage: fzf [options]
--preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window
--preview-wrap-sign=STR Indicator for wrapped lines in the preview window
HEADER
--header=STR String to print as header
@@ -608,6 +609,7 @@ type Options struct {
Wrap bool
WrapWord bool
WrapSign *string
PreviewWrapSign *string
MultiLine bool
CursorLine bool
KeepRight bool
@@ -3113,6 +3115,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
return err
}
case "--preview-wrap-sign":
str, err := nextString("preview wrap sign required")
if err != nil {
return err
}
opts.PreviewWrapSign = &str
case "--height":
str, err := nextString("height required: [~]HEIGHT[%]")
if err != nil {

View File

@@ -464,6 +464,50 @@ func TestPreviewOpts(t *testing.T) {
}
}
func TestPreviewWrapSign(t *testing.T) {
// Default: no preview wrap sign override
opts := optsFor()
if opts.PreviewWrapSign != nil {
t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
}
// --preview-wrap-sign sets PreviewWrapSign
opts = optsFor("--preview-wrap-sign", ">> ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign is independent of --wrap-sign
opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
if opts.WrapSign == nil || *opts.WrapSign != "| " {
t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
}
// --preview-wrap-sign without --wrap-sign
opts = optsFor("--preview-wrap-sign", "→ ")
if opts.WrapSign != nil {
t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
}
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
}
// Last --preview-wrap-sign wins
opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
}
// Empty string is allowed
opts = optsFor("--preview-wrap-sign", "")
if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
}
}
func TestAdditiveExpect(t *testing.T) {
opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
if len(opts.Expect) != 3 {

View File

@@ -245,201 +245,203 @@ type runningCmd struct {
// Terminal represents terminal input/output
type Terminal struct {
initDelay time.Duration
infoCommand string
infoStyle infoStyle
infoPrefix string
wrap bool
wrapWord bool
wrapSign string
wrapSignWidth int
ghost string
separator labelPrinter
separatorLen int
spinner []string
promptString string
prompt func()
promptLen int
borderLabel labelPrinter
borderLabelLen int
borderLabelOpts labelOpts
previewLabel labelPrinter
previewLabelLen int
previewLabelOpts labelOpts
inputLabel labelPrinter
inputLabelLen int
inputLabelOpts labelOpts
headerLabel labelPrinter
headerLabelLen int
headerLabelOpts labelOpts
footerLabel labelPrinter
footerLabelLen int
footerLabelOpts labelOpts
gutterReverse bool
gutterRawReverse bool
pointer string
pointerLen int
pointerEmpty string
pointerEmptyRaw string
marker string
markerLen int
markerEmpty string
markerMultiLine [3]string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
gap int
gapLine labelPrinter
gapLineLen int
wordRubout string
wordNext string
subWordRubout string
subWordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
inputOverride *[]rune
pasting *[]rune
multi int
multiLine bool
sort bool
toggleSort bool
track trackOption
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
keymapOrg map[tui.Event][]*action
pressed string
printQueue []string
printQuery bool
history *History
cycle bool
highlightLine bool
headerVisible bool
headerFirst bool
headerLines int
header []string
header0 []string
footer []string
ellipsis string
scrollbar string
previewScrollbar string
ansi bool
freezeLeft int
freezeRight int
nthAttr tui.Attr
nth []Range
nthCurrent []Range
acceptNth func([]Token, int32) string
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
listenAddr *listenAddress
listenPort *int
listener net.Listener
listenUnsafe bool
borderShape tui.BorderShape
listBorderShape tui.BorderShape
inputBorderShape tui.BorderShape
headerBorderShape tui.BorderShape
headerLinesShape tui.BorderShape
footerBorderShape tui.BorderShape
listLabel labelPrinter
listLabelLen int
listLabelOpts labelOpts
cleanExit bool
executor *util.Executor
paused bool
inputless bool
border tui.Window
window tui.Window
inputWindow tui.Window
inputBorder tui.Window
headerWindow tui.Window
headerBorder tui.Window
headerLinesWindow tui.Window
headerLinesBorder tui.Window
footerWindow tui.Window
footerBorder tui.Window
wborder tui.Window
pborder tui.Window
pwindow tui.Window
borderWidth int
count int
progress int
hasStartActions bool
hasResultActions bool
hasFocusActions bool
hasLoadActions bool
hasResizeActions bool
triggerLoad bool
reading bool
running *util.AtomicBool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
passMerger *Merger
resultMerger *Merger
matchMap map[int32]Result
selected map[int32]selectedItem
version int64
revision revision
bgVersion int64
runningCmds *util.ConcurrentSet[*runningCmd]
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
activePreviewOpts *previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
uiMutex sync.Mutex
initFunc func() error
prevLines []itemLine
suppress bool
startChan chan fitpad
killChan chan bool
killedChan chan bool
serverInputChan chan []*action
callbackChan chan versionedCallback
bgQueue map[action][]func(bool)
bgSemaphore chan struct{}
bgSemaphores map[action]chan struct{}
keyChan chan tui.Event
eventChan chan tui.Event
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
ttyDefault string
ttyin *os.File
executing *util.AtomicBool
termSize tui.TermSize
lastAction actionType
lastKey string
lastFocus int32
areaLines int
areaColumns int
forcePreview bool
clickHeaderLine int
clickHeaderColumn int
clickFooterLine int
clickFooterColumn int
proxyScript string
numLinesCache map[int32]numLinesCacheValue
raw bool
initDelay time.Duration
infoCommand string
infoStyle infoStyle
infoPrefix string
wrap bool
wrapWord bool
wrapSign string
wrapSignWidth int
previewWrapSign string
previewWrapSignWidth int
ghost string
separator labelPrinter
separatorLen int
spinner []string
promptString string
prompt func()
promptLen int
borderLabel labelPrinter
borderLabelLen int
borderLabelOpts labelOpts
previewLabel labelPrinter
previewLabelLen int
previewLabelOpts labelOpts
inputLabel labelPrinter
inputLabelLen int
inputLabelOpts labelOpts
headerLabel labelPrinter
headerLabelLen int
headerLabelOpts labelOpts
footerLabel labelPrinter
footerLabelLen int
footerLabelOpts labelOpts
gutterReverse bool
gutterRawReverse bool
pointer string
pointerLen int
pointerEmpty string
pointerEmptyRaw string
marker string
markerLen int
markerEmpty string
markerMultiLine [3]string
queryLen [2]int
layout layoutType
fullscreen bool
keepRight bool
hscroll bool
hscrollOff int
scrollOff int
gap int
gapLine labelPrinter
gapLineLen int
wordRubout string
wordNext string
subWordRubout string
subWordNext string
cx int
cy int
offset int
xoffset int
yanked []rune
input []rune
inputOverride *[]rune
pasting *[]rune
multi int
multiLine bool
sort bool
toggleSort bool
track trackOption
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
keymapOrg map[tui.Event][]*action
pressed string
printQueue []string
printQuery bool
history *History
cycle bool
highlightLine bool
headerVisible bool
headerFirst bool
headerLines int
header []string
header0 []string
footer []string
ellipsis string
scrollbar string
previewScrollbar string
ansi bool
freezeLeft int
freezeRight int
nthAttr tui.Attr
nth []Range
nthCurrent []Range
acceptNth func([]Token, int32) string
tabstop int
margin [4]sizeSpec
padding [4]sizeSpec
unicode bool
listenAddr *listenAddress
listenPort *int
listener net.Listener
listenUnsafe bool
borderShape tui.BorderShape
listBorderShape tui.BorderShape
inputBorderShape tui.BorderShape
headerBorderShape tui.BorderShape
headerLinesShape tui.BorderShape
footerBorderShape tui.BorderShape
listLabel labelPrinter
listLabelLen int
listLabelOpts labelOpts
cleanExit bool
executor *util.Executor
paused bool
inputless bool
border tui.Window
window tui.Window
inputWindow tui.Window
inputBorder tui.Window
headerWindow tui.Window
headerBorder tui.Window
headerLinesWindow tui.Window
headerLinesBorder tui.Window
footerWindow tui.Window
footerBorder tui.Window
wborder tui.Window
pborder tui.Window
pwindow tui.Window
borderWidth int
count int
progress int
hasStartActions bool
hasResultActions bool
hasFocusActions bool
hasLoadActions bool
hasResizeActions bool
triggerLoad bool
reading bool
running *util.AtomicBool
failed *string
jumping jumpMode
jumpLabels string
printer func(string)
printsep string
merger *Merger
passMerger *Merger
resultMerger *Merger
matchMap map[int32]Result
selected map[int32]selectedItem
version int64
revision revision
bgVersion int64
runningCmds *util.ConcurrentSet[*runningCmd]
reqBox *util.EventBox
initialPreviewOpts previewOpts
previewOpts previewOpts
activePreviewOpts *previewOpts
previewer previewer
previewed previewed
previewBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
uiMutex sync.Mutex
initFunc func() error
prevLines []itemLine
suppress bool
startChan chan fitpad
killChan chan bool
killedChan chan bool
serverInputChan chan []*action
callbackChan chan versionedCallback
bgQueue map[action][]func(bool)
bgSemaphore chan struct{}
bgSemaphores map[action]chan struct{}
keyChan chan tui.Event
eventChan chan tui.Event
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
ttyDefault string
ttyin *os.File
executing *util.AtomicBool
termSize tui.TermSize
lastAction actionType
lastKey string
lastFocus int32
areaLines int
areaColumns int
forcePreview bool
clickHeaderLine int
clickHeaderColumn int
clickFooterLine int
clickFooterColumn int
proxyScript string
numLinesCache map[int32]numLinesCacheValue
raw bool
}
type numLinesCacheValue struct {
@@ -1251,6 +1253,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
t.wrapSign = *opts.WrapSign
}
t.wrapSign, t.wrapSignWidth = t.processTabsStr(t.wrapSign, 0)
t.previewWrapSign = t.wrapSign
t.previewWrapSignWidth = t.wrapSignWidth
if opts.PreviewWrapSign != nil {
t.previewWrapSign, t.previewWrapSignWidth = t.processTabsStr(*opts.PreviewWrapSign, 0)
}
if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 {
t.scrollbar = "│"
@@ -2312,7 +2319,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
pwidth -= 1
}
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true)
t.pwindow.SetWrapSign(t.wrapSign, t.wrapSignWidth)
t.pwindow.SetWrapSign(t.previewWrapSign, t.previewWrapSignWidth)
if !hadPreviewWindow {
t.pwindow.Erase()
}
@@ -4130,14 +4137,14 @@ func (t *Terminal) previewLineHeight(line string, maxWidth int) int {
// For word-wrap mode, count the sub-lines produced by word wrapping.
// Each sub-line may still char-wrap if it contains a word longer than the width.
if t.activePreviewOpts.wrapWord {
subLines := t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth)
subLines := t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
total := 0
for i, sub := range subLines {
prefixWidth := 0
cols := maxWidth
if i > 0 {
prefixWidth = t.wrapSignWidth
cols -= t.wrapSignWidth
prefixWidth = t.previewWrapSignWidth
cols -= t.previewWrapSignWidth
}
w := t.ansiLineWidth(sub, prefixWidth)
if cols <= 0 {
@@ -4154,7 +4161,7 @@ func (t *Terminal) previewLineHeight(line string, maxWidth int) int {
return 1
}
remaining := w - maxWidth
contWidth := max(1, maxWidth-t.wrapSignWidth)
contWidth := max(1, maxWidth-t.previewWrapSignWidth)
return 1 + (remaining+contWidth-1)/contWidth
}
@@ -4333,7 +4340,7 @@ Loop:
// Pre-split line into sub-lines for word wrapping
var subLines []string
if t.activePreviewOpts.wrapWord {
subLines = t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth)
subLines = t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
} else {
subLines = []string{line}
}
@@ -4341,7 +4348,7 @@ Loop:
var fillRet tui.FillReturn
wrap := t.activePreviewOpts.wrap
printWrapSign := func() {
if t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, tui.Dim, t.wrapSign) == tui.FillNextLine {
if t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), -1, tui.Dim, t.previewWrapSign) == tui.FillNextLine {
t.pwindow.Move(t.pwindow.Y()-1, t.pwindow.Width())
}
fillRet = tui.FillContinue