From d51980a3f50dfa8ce43b01a3dce216afb8a0bd8f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2023 01:56:29 +0900 Subject: [PATCH] Add 'transform-border-label' and 'transform-preview-label' --- CHANGELOG.md | 7 ++- man/man1/fzf.1 | 140 ++++++++++++++++++++++++----------------------- src/options.go | 14 +++-- src/terminal.go | 86 +++++++++++++++++++---------- src/tui/light.go | 37 +++++++++---- src/tui/tcell.go | 30 ++++++---- src/tui/tui.go | 1 + test/test_go.rb | 12 +++- 8 files changed, 199 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1993845..08da4026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ CHANGELOG ========= -0.36.1 +0.37.0 ------ +- New actions + - `change-border-label` + - `change-preview-label` + - `transform-border-label` + - `transform-preview-label` - Bug fixes and improvements 0.36.0 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 832524f5..70faf8a3 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -972,91 +972,93 @@ e.g. .SS AVAILABLE ACTIONS: A key or an event can be bound to one or more of the following actions. - \fBACTION: DEFAULT BINDINGS (NOTES): - \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR - \fBaccept\fR \fIenter double-click\fR - \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) - \fBbackward-char\fR \fIctrl-b left\fR - \fBbackward-delete-char\fR \fIctrl-h bspace\fR - \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) - \fBbackward-kill-word\fR \fIalt-bs\fR - \fBbackward-word\fR \fIalt-b shift-left\fR - \fBbeginning-of-line\fR \fIctrl-a home\fR - \fBcancel\fR (clear query string if not empty, abort fzf otherwise) - \fBchange-preview(...)\fR (change \fB--preview\fR option) - \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') - \fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) - \fBchange-prompt(...)\fR (change prompt to the given string) - \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) - \fBchange-query(...)\fR (change query string to the given string) - \fBclear-screen\fR \fIctrl-l\fR - \fBclear-selection\fR (clear multi-selection) - \fBclose\fR (close preview window if open, abort fzf otherwise) - \fBclear-query\fR (clear query string) - \fBdelete-char\fR \fIdel\fR - \fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) + \fBACTION: DEFAULT BINDINGS (NOTES): + \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR + \fBaccept\fR \fIenter double-click\fR + \fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection) + \fBbackward-char\fR \fIctrl-b left\fR + \fBbackward-delete-char\fR \fIctrl-h bspace\fR + \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) + \fBbackward-kill-word\fR \fIalt-bs\fR + \fBbackward-word\fR \fIalt-b shift-left\fR + \fBbeginning-of-line\fR \fIctrl-a home\fR + \fBcancel\fR (clear query string if not empty, abort fzf otherwise) + \fBchange-preview(...)\fR (change \fB--preview\fR option) + \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') + \fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) + \fBchange-prompt(...)\fR (change prompt to the given string) + \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) + \fBchange-query(...)\fR (change query string to the given string) + \fBclear-screen\fR \fIctrl-l\fR + \fBclear-selection\fR (clear multi-selection) + \fBclose\fR (close preview window if open, abort fzf otherwise) + \fBclear-query\fR (clear query string) + \fBdelete-char\fR \fIdel\fR + \fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty) \fBdeselect\fR - \fBdeselect-all\fR (deselect all matches) - \fBdisable-search\fR (disable search functionality) - \fBdown\fR \fIctrl-j ctrl-n down\fR - \fBenable-search\fR (enable search functionality) - \fBend-of-line\fR \fIctrl-e end\fR - \fBexecute(...)\fR (see below for the details) - \fBexecute-silent(...)\fR (see below for the details) - \fBfirst\fR (move to the first match; same as \fBpos(1)\fR) - \fBforward-char\fR \fIctrl-f right\fR - \fBforward-word\fR \fIalt-f shift-right\fR + \fBdeselect-all\fR (deselect all matches) + \fBdisable-search\fR (disable search functionality) + \fBdown\fR \fIctrl-j ctrl-n down\fR + \fBenable-search\fR (enable search functionality) + \fBend-of-line\fR \fIctrl-e end\fR + \fBexecute(...)\fR (see below for the details) + \fBexecute-silent(...)\fR (see below for the details) + \fBfirst\fR (move to the first match; same as \fBpos(1)\fR) + \fBforward-char\fR \fIctrl-f right\fR + \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR - \fBjump\fR (EasyMotion-like 2-keystroke movement) - \fBjump-accept\fR (jump and accept) + \fBjump\fR (EasyMotion-like 2-keystroke movement) + \fBjump-accept\fR (jump and accept) \fBkill-line\fR - \fBkill-word\fR \fIalt-d\fR - \fBlast\fR (move to the last match; same as \fBpos(-1)\fR) - \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) - \fBnext-selected\fR (move to the next selected item) - \fBpage-down\fR \fIpgdn\fR - \fBpage-up\fR \fIpgup\fR + \fBkill-word\fR \fIalt-d\fR + \fBlast\fR (move to the last match; same as \fBpos(-1)\fR) + \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) + \fBnext-selected\fR (move to the next selected item) + \fBpage-down\fR \fIpgdn\fR + \fBpage-up\fR \fIpgup\fR \fBhalf-page-down\fR \fBhalf-page-up\fR - \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) - \fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR) - \fBprev-selected\fR (move to the previous selected item) - \fBpreview(...)\fR (see below for the details) - \fBpreview-down\fR \fIshift-down\fR - \fBpreview-up\fR \fIshift-up\fR + \fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end) + \fBprev-history\fR (\fIctrl-p\fR on \fB--history\fR) + \fBprev-selected\fR (move to the previous selected item) + \fBpreview(...)\fR (see below for the details) + \fBpreview-down\fR \fIshift-down\fR + \fBpreview-up\fR \fIshift-up\fR \fBpreview-page-down\fR \fBpreview-page-up\fR \fBpreview-half-page-down\fR \fBpreview-half-page-up\fR \fBpreview-bottom\fR \fBpreview-top\fR - \fBprint-query\fR (print query and exit) - \fBput\fR (put the character to the prompt) - \fBput(...)\fR (put the given string to the prompt) + \fBprint-query\fR (print query and exit) + \fBput\fR (put the character to the prompt) + \fBput(...)\fR (put the given string to the prompt) \fBrefresh-preview\fR - \fBrebind(...)\fR (rebind bindings after \fBunbind\fR) - \fBreload(...)\fR (see below for the details) - \fBreload-sync(...)\fR (see below for the details) - \fBreplace-query\fR (replace query string with the current selection) + \fBrebind(...)\fR (rebind bindings after \fBunbind\fR) + \fBreload(...)\fR (see below for the details) + \fBreload-sync(...)\fR (see below for the details) + \fBreplace-query\fR (replace query string with the current selection) \fBselect\fR - \fBselect-all\fR (select all matches) - \fBtoggle\fR (\fIright-click\fR) - \fBtoggle-all\fR (toggle all matches) - \fBtoggle+down\fR \fIctrl-i (tab)\fR - \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) - \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) + \fBselect-all\fR (select all matches) + \fBtoggle\fR (\fIright-click\fR) + \fBtoggle-all\fR (toggle all matches) + \fBtoggle+down\fR \fIctrl-i (tab)\fR + \fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) + \fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR \fBtoggle-preview-wrap\fR - \fBtoggle-search\fR (toggle search functionality) + \fBtoggle-search\fR (toggle search functionality) \fBtoggle-sort\fR - \fBtoggle+up\fR \fIbtab (shift-tab)\fR - \fBtransform-prompt(...)\fR (transform prompt string using an external command) - \fBtransform-query(...)\fR (transform query string using an external command) - \fBunbind(...)\fR (unbind bindings) - \fBunix-line-discard\fR \fIctrl-u\fR - \fBunix-word-rubout\fR \fIctrl-w\fR - \fBup\fR \fIctrl-k ctrl-p up\fR - \fByank\fR \fIctrl-y\fR + \fBtoggle+up\fR \fIbtab (shift-tab)\fR + \fBtransform-border-label(...)\fR (transform border label using an external command) + \fBtransform-preview-label(...)\fR (transform preview label using an external command) + \fBtransform-prompt(...)\fR (transform prompt string using an external command) + \fBtransform-query(...)\fR (transform query string using an external command) + \fBunbind(...)\fR (unbind bindings) + \fBunix-line-discard\fR \fIctrl-u\fR + \fBunix-word-rubout\fR \fIctrl-w\fR + \fBup\fR \fIctrl-k ctrl-p up\fR + \fByank\fR \fIctrl-y\fR .SS ACTION COMPOSITION diff --git a/src/options.go b/src/options.go index f22e767a..fa238d7e 100644 --- a/src/options.go +++ b/src/options.go @@ -912,7 +912,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt)|change-preview-window|change-preview-label|change-border-label|change-preview|(?:re|un)bind|pos|put)`) + `(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1220,16 +1220,16 @@ func isExecuteAction(str string) actionType { return actRebind case "preview": return actPreview + case "change-border-label": + return actChangeBorderLabel + case "change-preview-label": + return actChangePreviewLabel case "change-preview-window": return actChangePreviewWindow case "change-preview": return actChangePreview - case "change-preview-label": - return actChangePreviewLabel case "change-prompt": return actChangePrompt - case "change-border-label": - return actChangeBorderLabel case "change-query": return actChangeQuery case "pos": @@ -1242,6 +1242,10 @@ func isExecuteAction(str string) actionType { return actExecuteMulti case "put": return actPut + case "transform-border-label": + return actTransformBorderLabel + case "transform-preview-label": + return actTransformPreviewLabel case "transform-prompt": return actTransformPrompt case "transform-query": diff --git a/src/terminal.go b/src/terminal.go index 5b03f6d7..b2a99134 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -276,6 +276,8 @@ const ( reqRefresh reqReinit reqFullRedraw + reqRedrawBorderLabel + reqRedrawPreviewLabel reqClose reqPrintQuery reqPreviewEnqueue @@ -349,6 +351,8 @@ const ( actToggleSort actTogglePreview actTogglePreviewWrap + actTransformBorderLabel + actTransformPreviewLabel actTransformPrompt actTransformQuery actPreview @@ -1252,37 +1256,41 @@ func (t *Terminal) resizeWindows(forcePreview bool) { } // Print border label - printLabel := func(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape) { - if window == nil || render == nil { - return - } - - switch borderShape { - case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: - var col int - if opts.column == 0 { - col = util.Max(0, (window.Width()-length)/2) - } else if opts.column < 0 { - col = util.Max(0, window.Width()+opts.column+1-length) - } else { - col = util.Min(opts.column-1, window.Width()-length) - } - row := 0 - if borderShape == tui.BorderBottom || opts.bottom { - row = window.Height() - 1 - } - window.Move(row, col) - render(window, window.Width()) - } - } - printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape) - printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border) + t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, false) + t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, false) for i := 0; i < t.window.Height(); i++ { t.window.MoveAndClear(i, 0) } } +func (t *Terminal) printLabel(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape, redrawBorder bool) { + if window == nil || render == nil { + return + } + + switch borderShape { + case tui.BorderHorizontal, tui.BorderTop, tui.BorderBottom, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: + if redrawBorder { + window.DrawHBorder() + } + var col int + if opts.column == 0 { + col = util.Max(0, (window.Width()-length)/2) + } else if opts.column < 0 { + col = util.Max(0, window.Width()+opts.column+1-length) + } else { + col = util.Min(opts.column-1, window.Width()-length) + } + row := 0 + if borderShape == tui.BorderBottom || opts.bottom { + row = window.Height() - 1 + } + window.Move(row, col) + render(window, window.Width()) + } +} + func (t *Terminal) move(y int, x int, clear bool) { h := t.window.Height() @@ -2659,6 +2667,10 @@ func (t *Terminal) Loop() { t.printHeader() case reqRefresh: t.suppress = false + case reqRedrawBorderLabel: + t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true) + case reqRedrawPreviewLabel: + t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, true) case reqReinit: t.tui.Resume(t.fullscreen, t.sigstop) t.redraw() @@ -2912,11 +2924,27 @@ func (t *Terminal) Loop() { t.input = []rune(a.a) t.cx = len(t.input) case actChangeBorderLabel: - t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) - req(reqFullRedraw) + if t.border != nil { + t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) + req(reqRedrawBorderLabel) + } case actChangePreviewLabel: - t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) - req(reqFullRedraw) + if t.pborder != nil { + t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false) + req(reqRedrawPreviewLabel) + } + case actTransformBorderLabel: + if t.border != nil { + label := t.executeCommand(a.a, false, true, true) + t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) + req(reqRedrawBorderLabel) + } + case actTransformPreviewLabel: + if t.pborder != nil { + label := t.executeCommand(a.a, false, true, true) + t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) + req(reqRedrawPreviewLabel) + } case actChangePrompt: t.prompt, t.promptLen = t.parsePrompt(a.a) req(reqPrompt) diff --git a/src/tui/light.go b/src/tui/light.go index a336cac0..3ff4ded7 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -719,25 +719,38 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev w.fg = r.theme.Fg.Color w.bg = r.theme.Bg.Color } - w.drawBorder() + w.drawBorder(false) return w } -func (w *LightWindow) drawBorder() { +func (w *LightWindow) DrawHBorder() { + w.drawBorder(true) +} + +func (w *LightWindow) drawBorder(onlyHorizontal bool) { switch w.border.shape { case BorderRounded, BorderSharp, BorderBold, BorderDouble: - w.drawBorderAround() + w.drawBorderAround(onlyHorizontal) case BorderHorizontal: w.drawBorderHorizontal(true, true) case BorderVertical: + if onlyHorizontal { + return + } w.drawBorderVertical(true, true) case BorderTop: w.drawBorderHorizontal(true, false) case BorderBottom: w.drawBorderHorizontal(false, true) case BorderLeft: + if onlyHorizontal { + return + } w.drawBorderVertical(true, false) case BorderRight: + if onlyHorizontal { + return + } w.drawBorderVertical(false, true) } } @@ -779,23 +792,25 @@ func (w *LightWindow) drawBorderVertical(left, right bool) { } } -func (w *LightWindow) drawBorderAround() { +func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { w.Move(0, 0) color := ColBorder if w.preview { color = ColPreviewBorder } hw := runewidth.RuneWidth(w.border.horizontal) - vw := runewidth.RuneWidth(w.border.vertical) tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight) bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight) rem := (w.width - tcw) % hw w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight)) - for y := 1; y < w.height-1; y++ { - w.Move(y, 0) - w.CPrint(color, string(w.border.vertical)) - w.CPrint(color, repeat(' ', w.width-vw*2)) - w.CPrint(color, string(w.border.vertical)) + if !onlyHorizontal { + vw := runewidth.RuneWidth(w.border.vertical) + for y := 1; y < w.height-1; y++ { + w.Move(y, 0) + w.CPrint(color, string(w.border.vertical)) + w.CPrint(color, repeat(' ', w.width-vw*2)) + w.CPrint(color, string(w.border.vertical)) + } } w.Move(w.height-1, 0) rem = (w.width - bcw) % hw @@ -1040,7 +1055,7 @@ func (w *LightWindow) FinishFill() { } func (w *LightWindow) Erase() { - w.drawBorder() + w.drawBorder(false) // We don't erase the window here to avoid flickering during scroll w.Move(0, 0) } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 366cb775..ad0182cf 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -512,7 +512,7 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, height: height, normal: normal, borderStyle: borderStyle} - w.drawBorder() + w.drawBorder(false) return w } @@ -670,7 +670,11 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { return w.fillString(str, NewColorPair(fg, bg, a)) } -func (w *TcellWindow) drawBorder() { +func (w *TcellWindow) DrawHBorder() { + w.drawBorder(true) +} + +func (w *TcellWindow) drawBorder(onlyHorizontal bool) { shape := w.borderStyle.shape if shape == BorderNone { return @@ -718,17 +722,19 @@ func (w *TcellWindow) drawBorder() { _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) } } - switch shape { - case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft: - for y := top; y < bot; y++ { - _screen.SetContent(left, y, w.borderStyle.vertical, nil, style) + if !onlyHorizontal { + switch shape { + case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderLeft: + for y := top; y < bot; y++ { + _screen.SetContent(left, y, w.borderStyle.vertical, nil, style) + } } - } - switch shape { - case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight: - vw := runewidth.RuneWidth(w.borderStyle.vertical) - for y := top; y < bot; y++ { - _screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style) + switch shape { + case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight: + vw := runewidth.RuneWidth(w.borderStyle.vertical) + for y := top; y < bot; y++ { + _screen.SetContent(right-vw, y, w.borderStyle.vertical, nil, style) + } } } switch shape { diff --git a/src/tui/tui.go b/src/tui/tui.go index 5a86453b..203da76c 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -426,6 +426,7 @@ type Window interface { Width() int Height() int + DrawHBorder() Refresh() FinishFill() Close() diff --git a/test/test_go.rb b/test/test_go.rb index 5a3659d5..5bf5ab58 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2474,11 +2474,21 @@ class TestGoFZF < TestBase end def test_labels_center - tmux.send_keys ': | fzf --border --border-label foobar --preview : --preview-label barfoo', :Enter + tmux.send_keys 'echo x | fzf --border --border-label foobar --preview : --preview-label barfoo --bind "space:change-border-label(foobarfoo)+change-preview-label(barfoobar),enter:transform-border-label(echo foo{}foo)+transform-preview-label(echo bar{}bar)"', :Enter tmux.until do assert_includes(_1[0], '─foobar─') assert_includes(_1[1], '─barfoo─') end + tmux.send_keys :space + tmux.until do + assert_includes(_1[0], '─foobarfoo─') + assert_includes(_1[1], '─barfoobar─') + end + tmux.send_keys :Enter + tmux.until do + assert_includes(_1[0], '─fooxfoo─') + assert_includes(_1[1], '─barxbar─') + end end def test_labels_left