Add --preview-wrap-sign

This commit is contained in:
Junegunn Choi
2026-02-19 21:30:23 +09:00
parent 0ecbf3f475
commit 4a195e6323
6 changed files with 269 additions and 202 deletions
+2
View File
@@ -32,6 +32,8 @@ CHANGELOG
# In the preview window # In the preview window
fzf --preview "printf '\e[4:3;58;2;255;0;0mRed curly underline\e[0m\n'" 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 `alt-gutter` color option (#4602) (@hedgieinsocks)
- Added fish completion support (#4605) (@lalvarezt) - Added fish completion support (#4605) (@lalvarezt)
- zsh: Handle multi-line history selection (#4595) (@LangLangBart) - zsh: Handle multi-line history selection (#4595) (@LangLangBart)
+5
View File
@@ -919,6 +919,11 @@ Should be used with one of the following \fB\-\-preview\-window\fR options.
.B * border\-bottom .B * border\-bottom
.br .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 .TP
.BI "\-\-preview\-label\-pos" [=N[:top|bottom]] .BI "\-\-preview\-label\-pos" [=N[:top|bottom]]
Position of the border label on the border line of the preview window. Specify Position of the border label on the border line of the preview window. Specify
+1
View File
@@ -219,6 +219,7 @@ _fzf_opts_completion() {
--with-shell --with-shell
--wrap --wrap
--wrap-sign --wrap-sign
--preview-wrap-sign
--zsh --zsh
-0 --exit-0 -0 --exit-0
-1 --select-1 -1 --select-1
+8
View File
@@ -167,6 +167,7 @@ Usage: fzf [options]
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window but for preview window
--preview-wrap-sign=STR Indicator for wrapped lines in the preview window
HEADER HEADER
--header=STR String to print as header --header=STR String to print as header
@@ -608,6 +609,7 @@ type Options struct {
Wrap bool Wrap bool
WrapWord bool WrapWord bool
WrapSign *string WrapSign *string
PreviewWrapSign *string
MultiLine bool MultiLine bool
CursorLine bool CursorLine bool
KeepRight 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 { if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil {
return err return err
} }
case "--preview-wrap-sign":
str, err := nextString("preview wrap sign required")
if err != nil {
return err
}
opts.PreviewWrapSign = &str
case "--height": case "--height":
str, err := nextString("height required: [~]HEIGHT[%]") str, err := nextString("height required: [~]HEIGHT[%]")
if err != nil { if err != nil {
+44
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) { func TestAdditiveExpect(t *testing.T) {
opts := optsFor("--expect=a", "--expect", "b", "--expect=c") opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
if len(opts.Expect) != 3 { if len(opts.Expect) != 3 {
+209 -202
View File
@@ -245,201 +245,203 @@ type runningCmd struct {
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoCommand string infoCommand string
infoStyle infoStyle infoStyle infoStyle
infoPrefix string infoPrefix string
wrap bool wrap bool
wrapWord bool wrapWord bool
wrapSign string wrapSign string
wrapSignWidth int wrapSignWidth int
ghost string previewWrapSign string
separator labelPrinter previewWrapSignWidth int
separatorLen int ghost string
spinner []string separator labelPrinter
promptString string separatorLen int
prompt func() spinner []string
promptLen int promptString string
borderLabel labelPrinter prompt func()
borderLabelLen int promptLen int
borderLabelOpts labelOpts borderLabel labelPrinter
previewLabel labelPrinter borderLabelLen int
previewLabelLen int borderLabelOpts labelOpts
previewLabelOpts labelOpts previewLabel labelPrinter
inputLabel labelPrinter previewLabelLen int
inputLabelLen int previewLabelOpts labelOpts
inputLabelOpts labelOpts inputLabel labelPrinter
headerLabel labelPrinter inputLabelLen int
headerLabelLen int inputLabelOpts labelOpts
headerLabelOpts labelOpts headerLabel labelPrinter
footerLabel labelPrinter headerLabelLen int
footerLabelLen int headerLabelOpts labelOpts
footerLabelOpts labelOpts footerLabel labelPrinter
gutterReverse bool footerLabelLen int
gutterRawReverse bool footerLabelOpts labelOpts
pointer string gutterReverse bool
pointerLen int gutterRawReverse bool
pointerEmpty string pointer string
pointerEmptyRaw string pointerLen int
marker string pointerEmpty string
markerLen int pointerEmptyRaw string
markerEmpty string marker string
markerMultiLine [3]string markerLen int
queryLen [2]int markerEmpty string
layout layoutType markerMultiLine [3]string
fullscreen bool queryLen [2]int
keepRight bool layout layoutType
hscroll bool fullscreen bool
hscrollOff int keepRight bool
scrollOff int hscroll bool
gap int hscrollOff int
gapLine labelPrinter scrollOff int
gapLineLen int gap int
wordRubout string gapLine labelPrinter
wordNext string gapLineLen int
subWordRubout string wordRubout string
subWordNext string wordNext string
cx int subWordRubout string
cy int subWordNext string
offset int cx int
xoffset int cy int
yanked []rune offset int
input []rune xoffset int
inputOverride *[]rune yanked []rune
pasting *[]rune input []rune
multi int inputOverride *[]rune
multiLine bool pasting *[]rune
sort bool multi int
toggleSort bool multiLine bool
track trackOption sort bool
delimiter Delimiter toggleSort bool
expect map[tui.Event]string track trackOption
keymap map[tui.Event][]*action delimiter Delimiter
keymapOrg map[tui.Event][]*action expect map[tui.Event]string
pressed string keymap map[tui.Event][]*action
printQueue []string keymapOrg map[tui.Event][]*action
printQuery bool pressed string
history *History printQueue []string
cycle bool printQuery bool
highlightLine bool history *History
headerVisible bool cycle bool
headerFirst bool highlightLine bool
headerLines int headerVisible bool
header []string headerFirst bool
header0 []string headerLines int
footer []string header []string
ellipsis string header0 []string
scrollbar string footer []string
previewScrollbar string ellipsis string
ansi bool scrollbar string
freezeLeft int previewScrollbar string
freezeRight int ansi bool
nthAttr tui.Attr freezeLeft int
nth []Range freezeRight int
nthCurrent []Range nthAttr tui.Attr
acceptNth func([]Token, int32) string nth []Range
tabstop int nthCurrent []Range
margin [4]sizeSpec acceptNth func([]Token, int32) string
padding [4]sizeSpec tabstop int
unicode bool margin [4]sizeSpec
listenAddr *listenAddress padding [4]sizeSpec
listenPort *int unicode bool
listener net.Listener listenAddr *listenAddress
listenUnsafe bool listenPort *int
borderShape tui.BorderShape listener net.Listener
listBorderShape tui.BorderShape listenUnsafe bool
inputBorderShape tui.BorderShape borderShape tui.BorderShape
headerBorderShape tui.BorderShape listBorderShape tui.BorderShape
headerLinesShape tui.BorderShape inputBorderShape tui.BorderShape
footerBorderShape tui.BorderShape headerBorderShape tui.BorderShape
listLabel labelPrinter headerLinesShape tui.BorderShape
listLabelLen int footerBorderShape tui.BorderShape
listLabelOpts labelOpts listLabel labelPrinter
cleanExit bool listLabelLen int
executor *util.Executor listLabelOpts labelOpts
paused bool cleanExit bool
inputless bool executor *util.Executor
border tui.Window paused bool
window tui.Window inputless bool
inputWindow tui.Window border tui.Window
inputBorder tui.Window window tui.Window
headerWindow tui.Window inputWindow tui.Window
headerBorder tui.Window inputBorder tui.Window
headerLinesWindow tui.Window headerWindow tui.Window
headerLinesBorder tui.Window headerBorder tui.Window
footerWindow tui.Window headerLinesWindow tui.Window
footerBorder tui.Window headerLinesBorder tui.Window
wborder tui.Window footerWindow tui.Window
pborder tui.Window footerBorder tui.Window
pwindow tui.Window wborder tui.Window
borderWidth int pborder tui.Window
count int pwindow tui.Window
progress int borderWidth int
hasStartActions bool count int
hasResultActions bool progress int
hasFocusActions bool hasStartActions bool
hasLoadActions bool hasResultActions bool
hasResizeActions bool hasFocusActions bool
triggerLoad bool hasLoadActions bool
reading bool hasResizeActions bool
running *util.AtomicBool triggerLoad bool
failed *string reading bool
jumping jumpMode running *util.AtomicBool
jumpLabels string failed *string
printer func(string) jumping jumpMode
printsep string jumpLabels string
merger *Merger printer func(string)
passMerger *Merger printsep string
resultMerger *Merger merger *Merger
matchMap map[int32]Result passMerger *Merger
selected map[int32]selectedItem resultMerger *Merger
version int64 matchMap map[int32]Result
revision revision selected map[int32]selectedItem
bgVersion int64 version int64
runningCmds *util.ConcurrentSet[*runningCmd] revision revision
reqBox *util.EventBox bgVersion int64
initialPreviewOpts previewOpts runningCmds *util.ConcurrentSet[*runningCmd]
previewOpts previewOpts reqBox *util.EventBox
activePreviewOpts *previewOpts initialPreviewOpts previewOpts
previewer previewer previewOpts previewOpts
previewed previewed activePreviewOpts *previewOpts
previewBox *util.EventBox previewer previewer
eventBox *util.EventBox previewed previewed
mutex sync.Mutex previewBox *util.EventBox
uiMutex sync.Mutex eventBox *util.EventBox
initFunc func() error mutex sync.Mutex
prevLines []itemLine uiMutex sync.Mutex
suppress bool initFunc func() error
startChan chan fitpad prevLines []itemLine
killChan chan bool suppress bool
killedChan chan bool startChan chan fitpad
serverInputChan chan []*action killChan chan bool
callbackChan chan versionedCallback killedChan chan bool
bgQueue map[action][]func(bool) serverInputChan chan []*action
bgSemaphore chan struct{} callbackChan chan versionedCallback
bgSemaphores map[action]chan struct{} bgQueue map[action][]func(bool)
keyChan chan tui.Event bgSemaphore chan struct{}
eventChan chan tui.Event bgSemaphores map[action]chan struct{}
slab *util.Slab keyChan chan tui.Event
theme *tui.ColorTheme eventChan chan tui.Event
tui tui.Renderer slab *util.Slab
ttyDefault string theme *tui.ColorTheme
ttyin *os.File tui tui.Renderer
executing *util.AtomicBool ttyDefault string
termSize tui.TermSize ttyin *os.File
lastAction actionType executing *util.AtomicBool
lastKey string termSize tui.TermSize
lastFocus int32 lastAction actionType
areaLines int lastKey string
areaColumns int lastFocus int32
forcePreview bool areaLines int
clickHeaderLine int areaColumns int
clickHeaderColumn int forcePreview bool
clickFooterLine int clickHeaderLine int
clickFooterColumn int clickHeaderColumn int
proxyScript string clickFooterLine int
numLinesCache map[int32]numLinesCacheValue clickFooterColumn int
raw bool proxyScript string
numLinesCache map[int32]numLinesCacheValue
raw bool
} }
type numLinesCacheValue struct { type numLinesCacheValue struct {
@@ -1251,6 +1253,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
t.wrapSign = *opts.WrapSign t.wrapSign = *opts.WrapSign
} }
t.wrapSign, t.wrapSignWidth = t.processTabsStr(t.wrapSign, 0) 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 opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 { if t.unicode && t.borderWidth == 1 {
t.scrollbar = "│" t.scrollbar = "│"
@@ -2312,7 +2319,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
pwidth -= 1 pwidth -= 1
} }
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, tui.WindowPreview, noBorder, true) 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 { if !hadPreviewWindow {
t.pwindow.Erase() 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. // 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. // Each sub-line may still char-wrap if it contains a word longer than the width.
if t.activePreviewOpts.wrapWord { if t.activePreviewOpts.wrapWord {
subLines := t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth) subLines := t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
total := 0 total := 0
for i, sub := range subLines { for i, sub := range subLines {
prefixWidth := 0 prefixWidth := 0
cols := maxWidth cols := maxWidth
if i > 0 { if i > 0 {
prefixWidth = t.wrapSignWidth prefixWidth = t.previewWrapSignWidth
cols -= t.wrapSignWidth cols -= t.previewWrapSignWidth
} }
w := t.ansiLineWidth(sub, prefixWidth) w := t.ansiLineWidth(sub, prefixWidth)
if cols <= 0 { if cols <= 0 {
@@ -4154,7 +4161,7 @@ func (t *Terminal) previewLineHeight(line string, maxWidth int) int {
return 1 return 1
} }
remaining := w - maxWidth remaining := w - maxWidth
contWidth := max(1, maxWidth-t.wrapSignWidth) contWidth := max(1, maxWidth-t.previewWrapSignWidth)
return 1 + (remaining+contWidth-1)/contWidth return 1 + (remaining+contWidth-1)/contWidth
} }
@@ -4333,7 +4340,7 @@ Loop:
// Pre-split line into sub-lines for word wrapping // Pre-split line into sub-lines for word wrapping
var subLines []string var subLines []string
if t.activePreviewOpts.wrapWord { if t.activePreviewOpts.wrapWord {
subLines = t.wordWrapAnsiLine(line, maxWidth, t.wrapSignWidth) subLines = t.wordWrapAnsiLine(line, maxWidth, t.previewWrapSignWidth)
} else { } else {
subLines = []string{line} subLines = []string{line}
} }
@@ -4341,7 +4348,7 @@ Loop:
var fillRet tui.FillReturn var fillRet tui.FillReturn
wrap := t.activePreviewOpts.wrap wrap := t.activePreviewOpts.wrap
printWrapSign := func() { 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()) t.pwindow.Move(t.pwindow.Y()-1, t.pwindow.Width())
} }
fillRet = tui.FillContinue fillRet = tui.FillContinue