From 4a195e6323c4c47a653ac8611118ea888dcbdf6d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Feb 2026 21:29:03 +0900 Subject: [PATCH] Add --preview-wrap-sign --- CHANGELOG.md | 2 + man/man1/fzf.1 | 5 + shell/completion.bash | 1 + src/options.go | 8 + src/options_test.go | 44 +++++ src/terminal.go | 411 +++++++++++++++++++++--------------------- 6 files changed, 269 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b26aab..6c55d487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1864e673..03a40c01 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 diff --git a/shell/completion.bash b/shell/completion.bash index 13df0edb..ca8d398b 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -219,6 +219,7 @@ _fzf_opts_completion() { --with-shell --wrap --wrap-sign + --preview-wrap-sign --zsh -0 --exit-0 -1 --select-1 diff --git a/src/options.go b/src/options.go index f8066020..84c9a525 100644 --- a/src/options.go +++ b/src/options.go @@ -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 { diff --git a/src/options_test.go b/src/options_test.go index 010003cc..a8c42cef 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -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 { diff --git a/src/terminal.go b/src/terminal.go index 25d60c32..30f6992b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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