From 0c5956c43c1604d8e4364f08369dc4c58295ef3c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2023 01:22:02 +0900 Subject: [PATCH] Better support for Windows terminals * Default border style on Windows is changed to `sharp` because some Windows terminals are not capable of displaying `rounded` border characters correctly. * If your terminal emulator renders each box-drawing character with 2 columns, set `RUNEWIDTH_EASTASIAN` environment variable to `1`. --- CHANGELOG.md | 9 +++++++-- man/man1/fzf.1 | 8 ++++++-- src/options.go | 4 ++-- src/terminal.go | 35 +++++++++++++++++++++-------------- src/tui/dummy.go | 3 +++ src/tui/light.go | 19 ++++++++++++++----- src/tui/tcell.go | 17 ++++++++++++----- src/tui/tui.go | 1 + 8 files changed, 66 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04b276f..f259cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,10 @@ CHANGELOG # a will put 'alpha' on the prompt, ctrl-b will put 'bravo' fzf --bind 'a:put+put(lpha),ctrl-b:put(bravo)' ``` +- Added color name `preview-label` for `--preview-label` (defaults to `label` + for `--border-label`) +- Better support for (Windows) terminals where each box-drawing character + takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`. - Behavior changes - fzf will always execute the preview command if the command template contains `{q}` even when it's empty. If you prefer the old behavior, @@ -114,8 +118,9 @@ CHANGELOG when the user manually scrolls the window, the following stops. With this version, fzf will resume following if the user scrolls the window to the bottom. -- Added color name `preview-label` for `--preview-label` (defaults to `label` - for `--border-label`) + - Default border style on Windows is changed to `sharp` because some + Windows terminals are not capable of displaying `rounded` border + characters correctly. - Minor bug fixes and improvements 0.35.1 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 75856d0..6d44b46 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -230,6 +230,10 @@ Draw border around the finder .BR none .br +If you use a terminal emulator where box-drawing characters take 2 columns, +try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is still +not properly rendered, set \fB--no-unicode\fR. + .TP .BI "--border-label" [=LABEL] Label to print on the horizontal border line. Should be used with one of the @@ -568,9 +572,9 @@ 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) +.B * border-rounded (default on non-Windows platforms) .br -.B * border-sharp +.B * border-sharp (default on Windows) .br .B * border-bold .br diff --git a/src/options.go b/src/options.go index fa51cc3..fd16036 100644 --- a/src/options.go +++ b/src/options.go @@ -314,7 +314,7 @@ type Options struct { } func defaultPreviewOpts(command string) previewOpts { - return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0, 0, nil} + return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil} } func defaultOptions() *Options { @@ -543,7 +543,7 @@ func parseBorder(str string, optional bool) tui.BorderShape { return tui.BorderNone default: if optional && str == "" { - return tui.BorderRounded + return tui.DefaultBorderShape } errorExit("invalid border style (expected: rounded|sharp|bold|double|horizontal|vertical|top|bottom|left|right|none)") } diff --git a/src/terminal.go b/src/terminal.go index 8cd9a14..41b24f4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -210,6 +210,7 @@ type Terminal struct { window tui.Window pborder tui.Window pwindow tui.Window + borderWidth int count int progress int hasLoadActions bool @@ -604,6 +605,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { unicode: opts.Unicode, listenPort: opts.ListenPort, borderShape: opts.BorderShape, + borderWidth: 1, borderLabel: nil, borderLabelOpts: opts.BorderLabel, previewLabel: nil, @@ -666,8 +668,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true) } + if t.unicode { + t.borderWidth = runewidth.RuneWidth('│') + } if opts.Scrollbar == nil { - if t.unicode { + if t.unicode && t.borderWidth == 1 { t.scrollbar = "│" } else { t.scrollbar = "|" @@ -968,20 +973,21 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { paddingInt[idx] = sizeSpecToInt(idx, sizeSpec) } + bw := t.borderWidth extraMargin := [4]int{} // TRBL for idx, sizeSpec := range t.margin { switch t.borderShape { case tui.BorderHorizontal: extraMargin[idx] += 1 - idx%2 case tui.BorderVertical: - extraMargin[idx] += 2 * (idx % 2) + extraMargin[idx] += (1 + bw) * (idx % 2) case tui.BorderTop: if idx == 0 { extraMargin[idx]++ } case tui.BorderRight: if idx == 1 { - extraMargin[idx] += 2 + extraMargin[idx] += 1 + bw } case tui.BorderBottom: if idx == 2 { @@ -989,10 +995,10 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { } case tui.BorderLeft: if idx == 3 { - extraMargin[idx] += 2 + extraMargin[idx] += 1 + bw } case tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: - extraMargin[idx] += 1 + idx%2 + extraMargin[idx] += 1 + bw*(idx%2) } marginInt[idx] = sizeSpecToInt(idx, sizeSpec) + extraMargin[idx] } @@ -1106,6 +1112,7 @@ func (t *Terminal) resizeWindows(forcePreview bool) { return } hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil + bw := t.borderWidth createPreviewWindow := func(y int, x int, w int, h int) { pwidth := w pheight := h @@ -1118,15 +1125,15 @@ func (t *Terminal) resizeWindows(forcePreview bool) { t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) switch previewOpts.border { case tui.BorderSharp, tui.BorderRounded, tui.BorderBold, tui.BorderDouble: - pwidth -= 4 + pwidth -= (1 + bw) * 2 pheight -= 2 - x += 2 + x += 1 + bw y += 1 case tui.BorderLeft: - pwidth -= 2 - x += 2 + pwidth -= 1 + bw + x += 1 + bw case tui.BorderRight: - pwidth -= 2 + pwidth -= 1 + bw case tui.BorderTop: pheight -= 1 y += 1 @@ -1136,8 +1143,8 @@ func (t *Terminal) resizeWindows(forcePreview bool) { pheight -= 2 y += 1 case tui.BorderVertical: - pwidth -= 4 - x += 2 + pwidth -= (1 + bw) * 2 + x += 1 + bw } if len(t.scrollbar) > 0 && !previewOpts.border.HasRight() { // Need a column to show scrollbar @@ -1832,7 +1839,7 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) if len(t.previewer.bar) != height { t.previewer.bar = make([]bool, height) } - xshift := -2 + xshift := -1 - t.borderWidth if !t.previewOpts.border.HasRight() { xshift = -1 } @@ -1846,7 +1853,7 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int) // Avoid unnecessary redraws bar := i >= yoff+barStart && i < yoff+barStart+barLength - if bar == t.previewer.bar[i] { + if bar == t.previewer.bar[i] && !t.tui.NeedScrollbarRedraw() { continue } diff --git a/src/tui/dummy.go b/src/tui/dummy.go index adecd6f..7a02a8a 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -8,6 +8,8 @@ func HasFullscreenRenderer() bool { return false } +var DefaultBorderShape BorderShape = BorderRounded + func (a Attr) Merge(b Attr) Attr { return a | b } @@ -32,6 +34,7 @@ func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} func (r *FullscreenRenderer) Pause(bool) {} func (r *FullscreenRenderer) Resume(bool, bool) {} func (r *FullscreenRenderer) Clear() {} +func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false } func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Close() {} diff --git a/src/tui/light.go b/src/tui/light.go index c41de48..578118b 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -651,6 +651,10 @@ func (r *LightRenderer) Clear() { r.flush() } +func (r *LightRenderer) NeedScrollbarRedraw() bool { + return false +} + func (r *LightRenderer) RefreshWindows(windows []Window) { r.flush() } @@ -743,13 +747,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { if w.preview { color = ColPreviewBorder } + hw := runewidth.RuneWidth(w.border.horizontal) if top { w.Move(0, 0) - w.CPrint(color, repeat(w.border.horizontal, w.width)) + w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) } if bottom { w.Move(w.height-1, 0) - w.CPrint(color, repeat(w.border.horizontal, w.width)) + w.CPrint(color, repeat(w.border.horizontal, w.width/hw)) } } @@ -780,15 +785,19 @@ func (w *LightWindow) drawBorderAround() { if w.preview { color = ColPreviewBorder } - w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight)) + 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) + w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, (w.width-tcw)/hw)+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-2)) + w.CPrint(color, repeat(' ', w.width-vw*2)) w.CPrint(color, string(w.border.vertical)) } w.Move(w.height-1, 0) - w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight)) + w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+string(w.border.bottomRight)) } func (w *LightWindow) csi(code string) { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 82d7299..f1f18e2 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -17,6 +17,8 @@ func HasFullscreenRenderer() bool { return true } +var DefaultBorderShape BorderShape = BorderSharp + func asTcellColor(color Color) tcell.Color { if color == colDefault { return tcell.ColorDefault @@ -187,6 +189,10 @@ func (r *FullscreenRenderer) Clear() { _screen.Clear() } +func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { + return true +} + func (r *FullscreenRenderer) Refresh() { // noop } @@ -686,15 +692,16 @@ func (w *TcellWindow) drawBorder() { style = w.normal.style() } + hw := runewidth.RuneWidth(w.borderStyle.horizontal) switch shape { case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderTop: - for x := left; x < right; x++ { + for x := left; x <= right-hw; x += hw { _screen.SetContent(x, top, w.borderStyle.horizontal, nil, style) } } switch shape { case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderHorizontal, BorderBottom: - for x := left; x < right; x++ { + for x := left; x <= right-hw; x += hw { _screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style) } } @@ -707,14 +714,14 @@ func (w *TcellWindow) drawBorder() { switch shape { case BorderRounded, BorderSharp, BorderBold, BorderDouble, BorderVertical, BorderRight: for y := top; y < bot; y++ { - _screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style) + _screen.SetContent(right-hw, y, w.borderStyle.vertical, nil, style) } } switch shape { case BorderRounded, BorderSharp, BorderBold, BorderDouble: _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) - _screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style) + _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) - _screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style) + _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style) } } diff --git a/src/tui/tui.go b/src/tui/tui.go index c6a65d8..5a86453 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -410,6 +410,7 @@ type Renderer interface { RefreshWindows(windows []Window) Refresh() Close() + NeedScrollbarRedraw() bool GetChar() Event