From c09ec8e4d152dbbb6e0fe20f0182e4052cfe410e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 31 Oct 2022 00:22:41 +0900 Subject: [PATCH] Allow putting border label on the bottom line Related #3022 --- CHANGELOG.md | 11 +- man/man1/fzf.1 | 48 ++++++-- src/options.go | 298 ++++++++++++++++++++++++++---------------------- src/terminal.go | 49 ++++---- 4 files changed, 229 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42f70e3..30ed465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,15 +16,18 @@ CHANGELOG 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 + fzf --height=10 --border --border-label="╢ $label ╟" --color=label:italic:black # Left-aligned (positive integer) - fzf --height=10 --border-label="╢ $label ╟" --border=top --border-label-pos=3 --color=label:italic:black + fzf --height=10 --border --border-label="╢ $label ╟" --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 + # Right-aligned (negative integer) on the bottom line (:bottom) + fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=-3:bottom --color=label:italic:black ``` - Also added `--preview-label` and `--preview-label-pos` for the border of the + ```sh + fzf --preview 'cat {}' --border --preview-label=' Preview ' --preview-label-pos=2 + ``` preview window - Info panel (counter) will be followed by a horizontal separator by default - The color of the separator can be customized via `--color=separator:...` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 9d46d50..8a534ee 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -250,20 +250,21 @@ e.g. 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 + fzf --height=10 --border --border-label="╢ $label ╟" --color=label:italic:black # Left-aligned (positive integer) - fzf --height=10 --border-label="╢ $label ╟" --border=top --border-label-pos=3 --color=label:italic:black + fzf --height=10 --border --border-label="╢ $label ╟" --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 + # Right-aligned (negative integer) on the bottom line (:bottom) + fzf --height=10 --border --border-label="╢ $label ╟" --border-label-pos=-3:bottom --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. +.BI "--border-label-pos" [=N[:top|bottom]] +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. Label is printed on the top border line by default, add +\fB:bottom\fR to put it on the border line on the bottom. The default value +\fB0 (or \fBcenter\fR) will put the label at the center of the border line. .TP .B "--no-unicode" @@ -396,7 +397,7 @@ color mappings. \fBinfo \fRInfo line (match counters) \fBseparator \fRHorizontal separator on info line (match counters) \fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR) - \fBlabel \fRBorder label (\fB--border-label\fR) + \fBlabel \fRBorder label (\fB--border-label\fR and \fB--preview-label\fR) \fBprompt \fRPrompt \fBpointer \fRPointer to the current line \fBmarker \fRMulti-select marker @@ -527,6 +528,33 @@ e.g. sleep 0.01 done'\fR .RE + +.TP +.BI "--preview-label" [=LABEL] +Label to print on the horizontal border line of the preview window. +Should be used with one of the following \fB--preview-window\fR options. + +.br +.B * border-rounded (default) +.br +.B * border-sharp +.br +.B * border-horizontal +.br +.B * border-top +.br +.B * border-bottom +.br + +.TP +.BI "--preview-label-pos" [=N[:top|bottom]] +Position of the border label on the border line of the preview window. Specify +a positive integer as the column position from the left. Specify a negative +integer to right-align the label. Label is printed on the top border line by +default, add \fB:bottom\fR to put it on the border line on the bottom. The +default value 0 (or \fBcenter\fR) will put the label at the center of the +border line. + .TP .BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][, ", - Pointer: ">", - Marker: ">", - Query: "", - Select1: false, - Exit0: false, - Filter: nil, - ToggleSort: false, - Expect: make(map[tui.Event]string), - Keymap: make(map[tui.Event][]*action), - Preview: defaultPreviewOpts(""), - PrintQuery: false, - ReadZero: false, - Printer: func(str string) { fmt.Println(str) }, - PrintSep: "\n", - Sync: false, - History: nil, - Header: make([]string, 0), - HeaderLines: 0, - HeaderFirst: false, - Ellipsis: "..", - Margin: defaultMargin(), - Padding: defaultMargin(), - Unicode: true, - Tabstop: 8, - Label: "", - LabelPos: 0, - PLabel: "", - PLabelPos: 0, - ClearOnExit: true, - Version: false} + Fuzzy: true, + FuzzyAlgo: algo.FuzzyMatchV2, + Scheme: "default", + Extended: true, + Phony: false, + Case: CaseSmart, + Normalize: true, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: Delimiter{}, + Sort: 1000, + Tac: false, + Criteria: []criterion{byScore, byLength}, + Multi: 0, + Ansi: false, + Mouse: true, + Theme: tui.EmptyTheme(), + Black: false, + Bold: true, + MinHeight: 10, + Layout: layoutDefault, + Cycle: false, + KeepRight: false, + Hscroll: true, + HscrollOff: 10, + ScrollOff: 0, + FileWord: false, + InfoStyle: infoStyle{layout: infoDefault, separator: true}, + JumpLabels: defaultJumpLabels, + Prompt: "> ", + Pointer: ">", + Marker: ">", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + ToggleSort: false, + Expect: make(map[tui.Event]string), + Keymap: make(map[tui.Event][]*action), + Preview: defaultPreviewOpts(""), + PrintQuery: false, + ReadZero: false, + Printer: func(str string) { fmt.Println(str) }, + PrintSep: "\n", + Sync: false, + History: nil, + Header: make([]string, 0), + HeaderLines: 0, + HeaderFirst: false, + Ellipsis: "..", + Margin: defaultMargin(), + Padding: defaultMargin(), + Unicode: true, + Tabstop: 8, + BorderLabel: labelOpts{}, + PreviewLabel: labelOpts{}, + ClearOnExit: true, + Version: false} } func help(code int) { @@ -847,7 +861,10 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { return theme } -var executeRegexp *regexp.Regexp +var ( + executeRegexp *regexp.Regexp + splitRegexp *regexp.Regexp +) func firstKey(keymap map[tui.Event]string) tui.Event { for k := range keymap { @@ -867,6 +884,7 @@ func init() { // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) + splitRegexp = regexp.MustCompile("[,:]+") } func parseKeymap(keymap map[tui.Event][]*action, str string) { @@ -1229,7 +1247,7 @@ func parseInfoStyle(str string) infoStyle { layout := infoDefault separator := true - for _, token := range regexp.MustCompile("[,:]").Split(strings.ToLower(str), -1) { + for _, token := range splitRegexp.Split(strings.ToLower(str), -1) { switch token { case "default": layout = infoDefault @@ -1594,16 +1612,20 @@ func parseOptions(opts *Options, allArgs []string) { case "--border": hasArg, arg := optionalNextString(allArgs, &i) opts.BorderShape = parseBorder(arg, !hasArg) + case "--no-border-label": + opts.BorderLabel.label = "" case "--border-label": - opts.Label = nextString(allArgs, &i, "label required") + opts.BorderLabel.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) + parseLabelPosition(&opts.BorderLabel, pos) + case "--no-preview-label": + opts.PreviewLabel.label = "" case "--preview-label": - opts.PLabel = nextString(allArgs, &i, "preview label required") + opts.PreviewLabel.label = nextString(allArgs, &i, "preview label required") case "--preview-label-pos": pos := nextString(allArgs, &i, "preview label position required (positive or negative integer or 'center')") - opts.PLabelPos = parseLabelPosition(pos) + parseLabelPosition(&opts.PreviewLabel, pos) case "--no-unicode": opts.Unicode = false case "--unicode": @@ -1640,13 +1662,13 @@ func parseOptions(opts *Options, allArgs []string) { } 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 + opts.BorderLabel.label = value } else if match, value := optString(arg, "--border-label-pos="); match { - opts.LabelPos = parseLabelPosition(value) + parseLabelPosition(&opts.BorderLabel, value) } else if match, value := optString(arg, "--preview-label="); match { - opts.PLabel = value + opts.PreviewLabel.label = value } else if match, value := optString(arg, "--preview-label-pos="); match { - opts.PLabelPos = parseLabelPosition(value) + parseLabelPosition(&opts.PreviewLabel, 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 2764ff8..57055c1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -114,12 +114,12 @@ type Terminal struct { spinner []string prompt func() promptLen int - borderLabel func() + borderLabel func(tui.Window) borderLabelLen int - borderLabelPos int - previewLabel func() + borderLabelOpts labelOpts + previewLabel func(tui.Window) previewLabelLen int - previewLabelPos int + previewLabelOpts labelOpts pointer string pointerLen int pointerEmpty string @@ -551,9 +551,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { unicode: opts.Unicode, borderShape: opts.BorderShape, borderLabel: nil, - borderLabelPos: opts.LabelPos, + borderLabelOpts: opts.BorderLabel, previewLabel: nil, - previewLabelPos: opts.PLabelPos, + previewLabelOpts: opts.PreviewLabel, cleanExit: opts.ClearOnExit, paused: opts.Phony, strong: strongAttr, @@ -597,12 +597,8 @@ 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) - } - if len(opts.PLabel) > 0 { - t.previewLabel, t.previewLabelLen = t.parseBorderLabel(opts.PLabel) - } + t.borderLabel, t.borderLabelLen = t.parseBorderLabel(opts.BorderLabel.label) + t.previewLabel, t.previewLabelLen = t.parseBorderLabel(opts.PreviewLabel.label) return &t } @@ -633,20 +629,23 @@ func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) { return fit, padHeight } -func (t *Terminal) parseBorderLabel(borderLabel string) (func(), int) { +func (t *Terminal) parseBorderLabel(borderLabel string) (func(tui.Window), int) { + if len(borderLabel) == 0 { + return nil, 0 + } 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() { + borderLabelFn := func(window tui.Window) { 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) + text, _ := t.trimRight(runes, window.Width()) + t.printColoredString(window, text, offsets, tui.ColBorderLabel) } borderLabelLen := runewidth.StringWidth(text) return borderLabelFn, borderLabelLen @@ -1052,7 +1051,7 @@ func (t *Terminal) resizeWindows() { } // Print border label - printLabel := func(window tui.Window, render func(), pos int, length int, borderShape tui.BorderShape) { + printLabel := func(window tui.Window, render func(tui.Window), opts labelOpts, length int, borderShape tui.BorderShape) { if window == nil || render == nil { return } @@ -1060,23 +1059,23 @@ func (t *Terminal) resizeWindows() { switch borderShape { case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp: var col int - if pos == 0 { + if opts.column == 0 { col = util.Max(0, (window.Width()-length)/2) - } else if pos < 0 { - col = util.Max(0, window.Width()+pos+1-length) + } else if opts.column < 0 { + col = util.Max(0, window.Width()+opts.column+1-length) } else { - col = util.Min(pos-1, window.Width()-length) + col = util.Min(opts.column-1, window.Width()-length) } row := 0 - if borderShape == tui.BorderBottom { + if borderShape == tui.BorderBottom || opts.bottom { row = window.Height() - 1 } window.Move(row, col) - render() + render(window) } } - printLabel(t.border, t.borderLabel, t.borderLabelPos, t.borderLabelLen, t.borderShape) - printLabel(t.pborder, t.previewLabel, t.previewLabelPos, t.previewLabelLen, t.previewOpts.border) + printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape) + printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border) for i := 0; i < t.window.Height(); i++ { t.window.MoveAndClear(i, 0)