From e61585f2f37c6b1ead971f448af8db26dff1502c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Oct 2022 00:12:01 +0900 Subject: [PATCH] Add --border-label and --border-label-pos Close #3022 --- CHANGELOG.md | 15 ++++ man/man1/fzf.1 | 40 +++++++++++ src/options.go | 180 +++++++++++++++++++++++++++--------------------- src/terminal.go | 60 ++++++++++++++-- src/tui/tui.go | 24 +++++-- 5 files changed, 233 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50a64c75..f4969421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ CHANGELOG ```sh seq 100 | fzf --multi --sync --bind 'start:last+select-all+preview(echo welcome)' ``` +- Added `--border-label` and `--border-label-pos` for putting label on the border + ```sh + # ANSI color codes are supported + # (with https://github.com/busyloop/lolcat) + label=$(curl -s http://metaphorpsum.com/sentences/1 | lolcat -f) + + # Border label at the center + fzf --height=10 --border-label="╢ $label ╟" --border --color=label:italic:black + + # Left-aligned (positive integer) + fzf --height=10 --border-label="╢ $label ╟" --border=top --border-label-pos=3 --color=label:italic:black + + # Right-aligned (negative integer) + fzf --height=10 --border-label="╢ $label ╟" --border=bottom --border-label-pos=-3 --color=label:italic:black + ``` 0.34.0 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 01a1672d..c6a17c82 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -226,6 +226,45 @@ Draw border around the finder .BR none .br +.TP +.BI "--border-label" [=LABEL] +Label to print on the horizontal border line. Should be used with one of the +following \fB--border\fR options. + +.br +.B * rounded +.br +.B * sharp +.br +.B * horizontal +.br +.BR "* top" " (up)" +.br +.BR "* bottom" " (down)" +.br + +.br +e.g. + \fB# ANSI color codes are supported + # (with https://github.com/busyloop/lolcat) + label=$(curl -s http://metaphorpsum.com/sentences/1 | lolcat -f) + + # Border label at the center + fzf --height=10 --border-label="╢ $label ╟" --border --color=label:italic:black + + # Left-aligned (positive integer) + fzf --height=10 --border-label="╢ $label ╟" --border=top --border-label-pos=3 --color=label:italic:black + + # Right-aligned (negative integer) + fzf --height=10 --border-label="╢ $label ╟" --border=bottom --border-label-pos=-3 --color=label:italic:black\fR + +.TP +.BI "--border-label-pos" [=COL] +Horizontal position of the border label on the border line. Specify a positive +integer as the column position from the left. Specify a negative integer to +right-align the label. The default value 0 (or \fBcenter\fR) will put +the label at the center of the border line. + .TP .B "--no-unicode" Use ASCII characters instead of Unicode box drawing characters to draw border @@ -356,6 +395,7 @@ color mappings. \fBdisabled \fRQuery string when search is disabled \fBinfo \fRInfo line (match counters) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) + \fBlabel \fRBorder label (\fB--border-label\fR) \fBprompt \fRPrompt \fBpointer \fRPointer to the current line \fBmarker \fRMulti-select marker diff --git a/src/options.go b/src/options.go index acf67281..80ca3e11 100644 --- a/src/options.go +++ b/src/options.go @@ -18,97 +18,101 @@ import ( const usage = `usage: fzf [options] Search - -x, --extended Extended-search mode - (enabled by default; +x or --no-extended to disable) - -e, --exact Enable Exact-match - -i Case-insensitive match (default: smart-case match) - +i Case-sensitive match - --scheme=SCHEME Scoring scheme [default|path|history] - --literal Do not normalize latin script letters before matching - -n, --nth=N[,..] Comma-separated list of field index expressions - for limiting search scope. Each can be a non-zero - integer or a range expression ([BEGIN]..[END]). - --with-nth=N[,..] Transform the presentation of each line using - field index expressions - -d, --delimiter=STR Field delimiter regex (default: AWK-style) - +s, --no-sort Do not sort the result - --tac Reverse the order of the input - --disabled Do not perform search - --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply - when the scores are tied [length|chunk|begin|end|index] - (default: length) + -x, --extended Extended-search mode + (enabled by default; +x or --no-extended to disable) + -e, --exact Enable Exact-match + -i Case-insensitive match (default: smart-case match) + +i Case-sensitive match + --scheme=SCHEME Scoring scheme [default|path|history] + --literal Do not normalize latin script letters before matching + -n, --nth=N[,..] Comma-separated list of field index expressions + for limiting search scope. Each can be a non-zero + integer or a range expression ([BEGIN]..[END]). + --with-nth=N[,..] Transform the presentation of each line using + field index expressions + -d, --delimiter=STR Field delimiter regex (default: AWK-style) + +s, --no-sort Do not sort the result + --tac Reverse the order of the input + --disabled Do not perform search + --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply + when the scores are tied [length|chunk|begin|end|index] + (default: length) Interface - -m, --multi[=MAX] Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - --bind=KEYBINDS Custom key bindings. Refer to the man page. - --cycle Enable cyclic scroll - --keep-right Keep the right end of the line visible on overflow - --scroll-off=LINES Number of screen lines to keep above or below when - scrolling to the top or to the bottom (default: 0) - --no-hscroll Disable horizontal scroll - --hscroll-off=COLS Number of screen columns to keep to the right of the - highlighted substring (default: 10) - --filepath-word Make word-wise movements respect path separators - --jump-labels=CHARS Label characters for jump and jump-accept + -m, --multi[=MAX] Enable multi-select with tab/shift-tab + --no-mouse Disable mouse + --bind=KEYBINDS Custom key bindings. Refer to the man page. + --cycle Enable cyclic scroll + --keep-right Keep the right end of the line visible on overflow + --scroll-off=LINES Number of screen lines to keep above or below when + scrolling to the top or to the bottom (default: 0) + --no-hscroll Disable horizontal scroll + --hscroll-off=COLS Number of screen columns to keep to the right of the + highlighted substring (default: 10) + --filepath-word Make word-wise movements respect path separators + --jump-labels=CHARS Label characters for jump and jump-accept Layout - --height=[~]HEIGHT[%] Display fzf window below the cursor with the given - height instead of using fullscreen. - If prefixed with '~', fzf will determine the height - according to the input size. - --min-height=HEIGHT Minimum height when --height is given in percent - (default: 10) - --layout=LAYOUT Choose layout: [default|reverse|reverse-list] - --border[=STYLE] Draw border around the finder - [rounded|sharp|horizontal|vertical| - top|bottom|left|right|none] (default: rounded) - --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) - --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) - --info=STYLE Finder info style [default|inline|hidden] - --prompt=STR Input prompt (default: '> ') - --pointer=STR Pointer to the current line (default: '>') - --marker=STR Multi-select marker (default: '>') - --header=STR String to print as header - --header-lines=N The first N lines of the input are treated as header - --header-first Print header before the prompt line - --ellipsis=STR Ellipsis to show when line is truncated (default: '..') + --height=[~]HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen. + If prefixed with '~', fzf will determine the height + according to the input size. + --min-height=HEIGHT Minimum height when --height is given in percent + (default: 10) + --layout=LAYOUT Choose layout: [default|reverse|reverse-list] + --border[=STYLE] Draw border around the finder + [rounded|sharp|horizontal|vertical| + top|bottom|left|right|none] (default: rounded) + --border-label=LABEL Label to print on the border + --border-label-pos=COL Position of the border label + [POSITIVE_INTEGER: columns from left| + NEGATIVE_INTEGER: columns from right] (default: 0) + --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) + --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) + --info=STYLE Finder info style [default|inline|hidden] + --prompt=STR Input prompt (default: '> ') + --pointer=STR Pointer to the current line (default: '>') + --marker=STR Multi-select marker (default: '>') + --header=STR String to print as header + --header-lines=N The first N lines of the input are treated as header + --header-first Print header before the prompt line + --ellipsis=STR Ellipsis to show when line is truncated (default: '..') Display - --ansi Enable processing of ANSI color codes - --tabstop=SPACES Number of spaces for a tab character (default: 8) - --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors - --no-bold Do not use bold text + --ansi Enable processing of ANSI color codes + --tabstop=SPACES Number of spaces for a tab character (default: 8) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --no-bold Do not use bold text History - --history=FILE History file - --history-size=N Maximum number of history entries (default: 1000) + --history=FILE History file + --history-size=N Maximum number of history entries (default: 1000) Preview - --preview=COMMAND Command to preview highlighted line ({}) - --preview-window=OPT Preview window layout (default: right:50%) - [up|down|left|right][,SIZE[%]] - [,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden] - [,border-BORDER_OPT] - [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] - [,default][, 0 && (a.position == posUp || a.position == posDown) } @@ -258,6 +269,8 @@ type Options struct { Margin [4]sizeSpec Padding [4]sizeSpec BorderShape tui.BorderShape + Label string + LabelPos int Unicode bool Tabstop int ClearOnExit bool @@ -324,6 +337,8 @@ func defaultOptions() *Options { Padding: defaultMargin(), Unicode: true, Tabstop: 8, + Label: "", + LabelPos: 0, ClearOnExit: true, Version: false} } @@ -798,6 +813,8 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { mergeAttr(&theme.CurrentMatch) case "border": mergeAttr(&theme.Border) + case "label": + mergeAttr(&theme.BorderLabel) case "prompt": mergeAttr(&theme.Prompt) case "spinner": @@ -1556,6 +1573,11 @@ func parseOptions(opts *Options, allArgs []string) { case "--border": hasArg, arg := optionalNextString(allArgs, &i) opts.BorderShape = parseBorder(arg, !hasArg) + case "--border-label": + opts.Label = nextString(allArgs, &i, "label required") + case "--border-label-pos": + pos := nextString(allArgs, &i, "label position required (positive or negative integer or 'center')") + opts.LabelPos = parseLabelPosition(pos) case "--no-unicode": opts.Unicode = false case "--unicode": @@ -1591,6 +1613,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Delimiter = delimiterRegexp(value) } else if match, value := optString(arg, "--border="); match { opts.BorderShape = parseBorder(value, false) + } else if match, value := optString(arg, "--border-label="); match { + opts.Label = value + } else if match, value := optString(arg, "--border-label-pos="); match { + opts.LabelPos = parseLabelPosition(value) } else if match, value := optString(arg, "--prompt="); match { opts.Prompt = value } else if match, value := optString(arg, "--pointer="); match { diff --git a/src/terminal.go b/src/terminal.go index 369bc418..c28b5335 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -114,6 +114,9 @@ type Terminal struct { spinner []string prompt func() promptLen int + borderLabel func() + borderLabelLen int + borderLabelPos int pointer string pointerLen int pointerEmpty string @@ -544,6 +547,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { padding: opts.Padding, unicode: opts.Unicode, borderShape: opts.BorderShape, + borderLabel: nil, + borderLabelPos: opts.LabelPos, cleanExit: opts.ClearOnExit, paused: opts.Phony, strong: strongAttr, @@ -587,6 +592,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { // Pre-calculated empty pointer and marker signs t.pointerEmpty = strings.Repeat(" ", t.pointerLen) t.markerEmpty = strings.Repeat(" ", t.markerLen) + if len(opts.Label) > 0 { + t.borderLabel, t.borderLabelLen = t.parseBorderLabel(opts.Label) + } return &t } @@ -617,6 +625,25 @@ func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) { return fit, padHeight } +func (t *Terminal) parseBorderLabel(borderLabel string) (func(), int) { + text, colors, _ := extractColor(borderLabel, nil, nil) + runes := []rune(text) + item := &Item{text: util.RunesToChars(runes), colors: colors} + result := Result{item: item} + + var offsets []colorOffset + borderLabelFn := func() { + if offsets == nil { + // tui.Col* are not initialized until renderer.Init() + offsets = result.colorOffsets(nil, t.theme, tui.ColBorderLabel, tui.ColBorderLabel, false) + } + text, _ := t.trimRight(runes, t.border.Width()) + t.printColoredString(t.border, text, offsets, tui.ColBorderLabel) + } + borderLabelLen := runewidth.StringWidth(text) + return borderLabelFn, borderLabelLen +} + func (t *Terminal) parsePrompt(prompt string) (func(), int) { var state *ansiState trimmed, colors, _ := extractColor(prompt, state, nil) @@ -911,6 +938,27 @@ func (t *Terminal) resizeWindows() { false, tui.MakeBorderStyle(t.borderShape, t.unicode)) } + // Print border label + if t.border != nil && t.borderLabel != nil { + switch t.borderShape { + case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp: + var col int + if t.borderLabelPos == 0 { + col = util.Max(0, (t.border.Width()-t.borderLabelLen)/2) + } else if t.borderLabelPos < 0 { + col = util.Max(0, t.border.Width()+t.borderLabelPos+1-t.borderLabelLen) + } else { + col = util.Min(t.borderLabelPos-1, t.border.Width()-t.borderLabelLen) + } + row := 0 + if t.borderShape == tui.BorderBottom { + row = t.border.Height() - 1 + } + t.border.Move(row, col) + t.borderLabel() + } + } + // Add padding to margin for idx, val := range paddingInt { marginInt[idx] += val @@ -1394,6 +1442,11 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat displayWidth = t.displayWidthWithLimit(text, 0, displayWidth) } + t.printColoredString(t.window, text, offsets, colBase) + return displayWidth +} + +func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []colorOffset, colBase tui.ColorPair) { var index int32 var substr string var prefixWidth int @@ -1403,11 +1456,11 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) - t.window.CPrint(colBase, substr) + window.CPrint(colBase, substr) if b < e { substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) - t.window.CPrint(offset.color, substr) + window.CPrint(offset.color, substr) } index = e @@ -1417,9 +1470,8 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat } if index < maxOffset { substr, _ = t.processTabs(text[index:], prefixWidth) - t.window.CPrint(colBase, substr) + window.CPrint(colBase, substr) } - return displayWidth } func (t *Terminal) renderPreviewSpinner() { diff --git a/src/tui/tui.go b/src/tui/tui.go index 90c5327e..1a9c748a 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -268,6 +268,7 @@ type ColorTheme struct { Selected ColorAttr Header ColorAttr Border ColorAttr + BorderLabel ColorAttr } type Event struct { @@ -441,6 +442,7 @@ var ( ColBorder ColorPair ColPreview ColorPair ColPreviewBorder ColorPair + ColBorderLabel ColorPair ) func EmptyTheme() *ColorTheme { @@ -463,7 +465,9 @@ func EmptyTheme() *ColorTheme { Cursor: ColorAttr{colUndefined, AttrUndefined}, Selected: ColorAttr{colUndefined, AttrUndefined}, Header: ColorAttr{colUndefined, AttrUndefined}, - Border: ColorAttr{colUndefined, AttrUndefined}} + Border: ColorAttr{colUndefined, AttrUndefined}, + BorderLabel: ColorAttr{colUndefined, AttrUndefined}, + } } func NoColorTheme() *ColorTheme { @@ -486,7 +490,9 @@ func NoColorTheme() *ColorTheme { Cursor: ColorAttr{colDefault, AttrRegular}, Selected: ColorAttr{colDefault, AttrRegular}, Header: ColorAttr{colDefault, AttrRegular}, - Border: ColorAttr{colDefault, AttrRegular}} + Border: ColorAttr{colDefault, AttrRegular}, + BorderLabel: ColorAttr{colDefault, AttrRegular}, + } } func errorExit(message string) { @@ -514,7 +520,9 @@ func init() { Cursor: ColorAttr{colRed, AttrUndefined}, Selected: ColorAttr{colMagenta, AttrUndefined}, Header: ColorAttr{colCyan, AttrUndefined}, - Border: ColorAttr{colBlack, AttrUndefined}} + Border: ColorAttr{colBlack, AttrUndefined}, + BorderLabel: ColorAttr{colWhite, AttrUndefined}, + } Dark256 = &ColorTheme{ Colored: true, Input: ColorAttr{colDefault, AttrUndefined}, @@ -534,7 +542,9 @@ func init() { Cursor: ColorAttr{161, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined}, Header: ColorAttr{109, AttrUndefined}, - Border: ColorAttr{59, AttrUndefined}} + Border: ColorAttr{59, AttrUndefined}, + BorderLabel: ColorAttr{145, AttrUndefined}, + } Light256 = &ColorTheme{ Colored: true, Input: ColorAttr{colDefault, AttrUndefined}, @@ -554,7 +564,9 @@ func init() { Cursor: ColorAttr{161, AttrUndefined}, Selected: ColorAttr{168, AttrUndefined}, Header: ColorAttr{31, AttrUndefined}, - Border: ColorAttr{145, AttrUndefined}} + Border: ColorAttr{145, AttrUndefined}, + BorderLabel: ColorAttr{59, AttrUndefined}, + } } func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { @@ -590,6 +602,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Header = o(baseTheme.Header, theme.Header) theme.Border = o(baseTheme.Border, theme.Border) + theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel) initPalette(theme) } @@ -622,6 +635,7 @@ func initPalette(theme *ColorTheme) { ColInfo = pair(theme.Info, theme.Bg) ColHeader = pair(theme.Header, theme.Bg) ColBorder = pair(theme.Border, theme.Bg) + ColBorderLabel = pair(theme.BorderLabel, theme.Bg) ColPreview = pair(theme.PreviewFg, theme.PreviewBg) ColPreviewBorder = pair(theme.Border, theme.PreviewBg) }