diff --git a/BUILD.md b/BUILD.md index 71066ef00c4..af0a04e90dc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -34,8 +34,8 @@ make release Third-party libraries used -------------------------- -- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - - Licensed under [MIT](http://mattn.mit-license.org) +- [rivo/uniseg](https://github.com/rivo/uniseg) + - Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-isatty](https://github.com/mattn/go-isatty) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25cabe8fe32..fe99cc61e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ CHANGELOG ' --preview 'seq {} 10000' --preview-window up ``` - And we're phasing out `{fzf:prompt}` and `{fzf:action}` +- Changed [mattn/go-runewidth](https://github.com/mattn/go-runewidth) dependency to [rivo/uniseg](https://github.com/rivo/uniseg) for accurate results + - Set `--ambidouble` if your terminal displays characters with East Asian Width Class Ambiguous (e.g. box-drawing characters for borders) as 2 columns - Bug fixes 0.45.0 diff --git a/go.mod b/go.mod index 18d4cd194d1..64e7e7cb5d7 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,9 @@ module github.com/junegunn/fzf require ( github.com/gdamore/tcell/v2 v2.5.4 - github.com/junegunn/go-runewidth v0.0.15-0.20240119074001-7d2ea235ec54 + github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2 github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-shellwords v1.0.12 - github.com/rivo/uniseg v0.4.4 github.com/saracen/walker v0.1.3 golang.org/x/sys v0.16.0 golang.org/x/term v0.16.0 @@ -15,6 +14,7 @@ require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/rivo/uniseg v0.4.4 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 6b920d53012..9945377d8fa 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k= github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw= -github.com/junegunn/go-runewidth v0.0.15-0.20240119074001-7d2ea235ec54 h1:eBbXiL4VPihi5C8DhESYtax8HYfgCDUkzVYigADhkzU= -github.com/junegunn/go-runewidth v0.0.15-0.20240119074001-7d2ea235ec54/go.mod h1:Mq6NazeZhIIQPMFoInCi35AktcN/MuW2elHsDK5N52w= +github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2 h1:oEwPBh29BPu1MaTsz2dM9bDrkOgKBoYFC0u6uY2izWo= +github.com/junegunn/uniseg v0.0.0-20240120174029-b504da4f6ed2/go.mod h1:ywqF55XaSE3/uS2tkJqVFKiE0oIYAXRvU2N7DU4y3XQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 55db6d6bfc9..6e6417c70a9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -260,9 +260,8 @@ Draw border around the finder .br If you use a terminal emulator where each box-drawing character takes -2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to -\fB0\fR or \fB1\fR. If the border is still not properly rendered, set -\fB--no-unicode\fR. +2 columns, try setting \fB--ambidouble\fR. If the border is still not properly +rendered, set \fB--no-unicode\fR. .TP .BI "--border-label" [=LABEL] @@ -313,6 +312,11 @@ the label. Label is printed on the top border line by default, add Use ASCII characters instead of Unicode drawing characters to draw borders, the spinner and the horizontal separator. +.TP +.B "--ambidouble" +Set this option if your terminal displays characters with East Asian Width +Class Ambiguous (e.g. box-drawing characters for borders) as 2 columns. + .TP .BI "--margin=" MARGIN Comma-separated expression for margins around the finder. diff --git a/src/options.go b/src/options.go index b2d702d2e83..fc614c5b316 100644 --- a/src/options.go +++ b/src/options.go @@ -11,8 +11,8 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" + "github.com/junegunn/uniseg" - "github.com/junegunn/go-runewidth" "github.com/mattn/go-shellwords" ) @@ -337,6 +337,7 @@ type Options struct { BorderLabel labelOpts PreviewLabel labelOpts Unicode bool + Ambidouble bool Tabstop int ListenAddr *listenAddress Unsafe bool @@ -406,6 +407,7 @@ func defaultOptions() *Options { Margin: defaultMargin(), Padding: defaultMargin(), Unicode: true, + Ambidouble: os.Getenv("RUNEWIDTH_EASTASIAN") == "1", Tabstop: 8, BorderLabel: labelOpts{}, PreviewLabel: labelOpts{}, @@ -1593,8 +1595,6 @@ func parseOptions(opts *Options, allArgs []string) { } } validateJumpLabels := false - validatePointer := false - validateMarker := false for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { @@ -1774,10 +1774,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Prompt = nextString(allArgs, &i, "prompt string required") case "--pointer": opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required")) - validatePointer = true case "--marker": opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required")) - validateMarker = true case "--sync": opts.Sync = true case "--no-sync": @@ -1845,6 +1843,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Unicode = false case "--unicode": opts.Unicode = true + case "--ambidouble": + opts.Ambidouble = true + case "--no-ambidouble": + opts.Ambidouble = false case "--margin": opts.Margin = parseMargin( "margin", @@ -1903,10 +1905,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Prompt = value } else if match, value := optString(arg, "--pointer="); match { opts.Pointer = firstLine(value) - validatePointer = true } else if match, value := optString(arg, "--marker="); match { opts.Marker = firstLine(value) - validateMarker = true } else if match, value := optString(arg, "-n", "--nth="); match { opts.Nth = splitNth(value) } else if match, value := optString(arg, "--with-nth="); match { @@ -2013,31 +2013,31 @@ func parseOptions(opts *Options, allArgs []string) { } } } - - if validatePointer { - if err := validateSign(opts.Pointer, "pointer"); err != nil { - errorExit(err.Error()) - } - } - - if validateMarker { - if err := validateSign(opts.Marker, "marker"); err != nil { - errorExit(err.Error()) - } - } } func validateSign(sign string, signOptName string) error { if sign == "" { return fmt.Errorf("%v cannot be empty", signOptName) } - if runewidth.StringWidth(sign) > 2 { + if uniseg.StringWidth(sign) > 2 { return fmt.Errorf("%v display width should be up to 2", signOptName) } return nil } func postProcessOptions(opts *Options) { + if opts.Ambidouble { + uniseg.EastAsianAmbiguousWidth = 2 + } + + if err := validateSign(opts.Pointer, "pointer"); err != nil { + errorExit(err.Error()) + } + + if err := validateSign(opts.Marker, "marker"); err != nil { + errorExit(err.Error()) + } + if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 { errorExit("--height option is currently not supported on this platform") } @@ -2048,7 +2048,7 @@ func postProcessOptions(opts *Options) { errorExit("--scrollbar should be given one or two characters") } for _, r := range runes { - if runewidth.RuneWidth(r) != 1 { + if uniseg.StringWidth(string(r)) != 1 { errorExit("scrollbar display width should be 1") } } diff --git a/src/terminal.go b/src/terminal.go index 7832cbbc2e9..b5763aba76c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -17,8 +17,7 @@ import ( "syscall" "time" - "github.com/junegunn/go-runewidth" - "github.com/rivo/uniseg" + "github.com/junegunn/uniseg" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" @@ -798,7 +797,7 @@ 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('│') + t.borderWidth = uniseg.StringWidth("│") } if opts.Scrollbar == nil { if t.unicode && t.borderWidth == 1 { diff --git a/src/tui/light.go b/src/tui/light.go index ae7cc0cb213..5092371dea7 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -10,8 +10,7 @@ import ( "time" "unicode/utf8" - "github.com/junegunn/go-runewidth" - "github.com/rivo/uniseg" + "github.com/junegunn/uniseg" "golang.org/x/term" ) @@ -804,7 +803,7 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) { if w.preview { color = ColPreviewBorder } - hw := runewidth.RuneWidth(w.border.top) + hw := runeWidth(w.border.top) if top { w.Move(0, 0) w.CPrint(color, repeat(w.border.top, w.width/hw)) @@ -842,13 +841,13 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { if w.preview { color = ColPreviewBorder } - hw := runewidth.RuneWidth(w.border.top) - tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight) - bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight) + hw := runeWidth(w.border.top) + tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight) + bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight) rem := (w.width - tcw) % hw w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight)) if !onlyHorizontal { - vw := runewidth.RuneWidth(w.border.left) + vw := runeWidth(w.border.left) for y := 1; y < w.height-1; y++ { w.Move(y, 0) w.CPrint(color, string(w.border.left)) @@ -1020,7 +1019,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin } else if rs[0] == '\r' { w++ } else { - w = runewidth.StringWidth(str) + w = uniseg.StringWidth(str) } width += w diff --git a/src/tui/tcell.go b/src/tui/tcell.go index d1180ac7df1..917dc9cb8f0 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -10,8 +10,7 @@ import ( "github.com/gdamore/tcell/v2/encoding" "github.com/junegunn/fzf/src/util" - "github.com/junegunn/go-runewidth" - "github.com/rivo/uniseg" + "github.com/junegunn/uniseg" ) func HasFullscreenRenderer() bool { @@ -738,7 +737,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) { style = w.normal.style() } - hw := runewidth.RuneWidth(w.borderStyle.top) + hw := runeWidth(w.borderStyle.top) switch shape { case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop: max := right - 2*hw @@ -773,7 +772,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) { } switch shape { case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight: - vw := runewidth.RuneWidth(w.borderStyle.right) + vw := runeWidth(w.borderStyle.right) for y := top; y < bot; y++ { _screen.SetContent(right-vw, y, w.borderStyle.right, nil, style) } @@ -782,8 +781,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) { switch shape { case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble: _screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) - _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) + _screen.SetContent(right-runeWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style) _screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style) - _screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style) + _screen.SetContent(right-runeWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style) } } diff --git a/src/tui/tui.go b/src/tui/tui.go index 74129c16af3..a937e2b406f 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -5,6 +5,8 @@ import ( "os" "strconv" "time" + + "github.com/junegunn/uniseg" ) // Types of user action @@ -812,3 +814,7 @@ func initPalette(theme *ColorTheme) { ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg) ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg) } + +func runeWidth(r rune) int { + return uniseg.StringWidth(string(r)) +} diff --git a/src/util/util.go b/src/util/util.go index 91884378474..4695081224f 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -6,14 +6,13 @@ import ( "strings" "time" - "github.com/junegunn/go-runewidth" + "github.com/junegunn/uniseg" "github.com/mattn/go-isatty" - "github.com/rivo/uniseg" ) // StringWidth returns string width where each CR/LF character takes 1 column func StringWidth(s string) int { - return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r") + return uniseg.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r") } // RunesWidth returns runes width @@ -165,7 +164,7 @@ func RepeatToFill(str string, length int, limit int) string { output := strings.Repeat(str, times) if rest > 0 { for _, r := range str { - rest -= runewidth.RuneWidth(r) + rest -= uniseg.StringWidth(string(r)) if rest < 0 { break }