Change mattn/go-runewidth dependency to rivo/uniseg for accurate results

Related #3588 #3588 #3567
This commit is contained in:
Junegunn Choi 2024-01-21 02:52:28 +09:00
parent 66546208b2
commit 16f6473938
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
11 changed files with 59 additions and 51 deletions

View File

@ -34,8 +34,8 @@ make release
Third-party libraries used Third-party libraries used
-------------------------- --------------------------
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - [rivo/uniseg](https://github.com/rivo/uniseg)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org) - Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty) - [mattn/go-isatty](https://github.com/mattn/go-isatty)

View File

@ -31,6 +31,8 @@ CHANGELOG
' --preview 'seq {} 10000' --preview-window up ' --preview 'seq {} 10000' --preview-window up
``` ```
- And we're phasing out `{fzf:prompt}` and `{fzf:action}` - 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 - Bug fixes
0.45.0 0.45.0

4
go.mod
View File

@ -2,10 +2,9 @@ module github.com/junegunn/fzf
require ( require (
github.com/gdamore/tcell/v2 v2.5.4 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-isatty v0.0.17
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.4
github.com/saracen/walker v0.1.3 github.com/saracen/walker v0.1.3
golang.org/x/sys v0.16.0 golang.org/x/sys v0.16.0
golang.org/x/term 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/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.14 // 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/sync v0.5.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
) )

4
go.sum
View File

@ -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/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 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw= 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/uniseg v0.0.0-20240120174029-b504da4f6ed2 h1:oEwPBh29BPu1MaTsz2dM9bDrkOgKBoYFC0u6uY2izWo=
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/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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=

View File

@ -260,9 +260,8 @@ Draw border around the finder
.br .br
If you use a terminal emulator where each box-drawing character takes If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to 2 columns, try setting \fB--ambidouble\fR. If the border is still not properly
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set rendered, set \fB--no-unicode\fR.
\fB--no-unicode\fR.
.TP .TP
.BI "--border-label" [=LABEL] .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, Use ASCII characters instead of Unicode drawing characters to draw borders,
the spinner and the horizontal separator. 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 .TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
Comma-separated expression for margins around the finder. Comma-separated expression for margins around the finder.

View File

@ -11,8 +11,8 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/uniseg"
"github.com/junegunn/go-runewidth"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
) )
@ -337,6 +337,7 @@ type Options struct {
BorderLabel labelOpts BorderLabel labelOpts
PreviewLabel labelOpts PreviewLabel labelOpts
Unicode bool Unicode bool
Ambidouble bool
Tabstop int Tabstop int
ListenAddr *listenAddress ListenAddr *listenAddress
Unsafe bool Unsafe bool
@ -406,6 +407,7 @@ func defaultOptions() *Options {
Margin: defaultMargin(), Margin: defaultMargin(),
Padding: defaultMargin(), Padding: defaultMargin(),
Unicode: true, Unicode: true,
Ambidouble: os.Getenv("RUNEWIDTH_EASTASIAN") == "1",
Tabstop: 8, Tabstop: 8,
BorderLabel: labelOpts{}, BorderLabel: labelOpts{},
PreviewLabel: labelOpts{}, PreviewLabel: labelOpts{},
@ -1593,8 +1595,6 @@ func parseOptions(opts *Options, allArgs []string) {
} }
} }
validateJumpLabels := false validateJumpLabels := false
validatePointer := false
validateMarker := false
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@ -1774,10 +1774,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Prompt = nextString(allArgs, &i, "prompt string required") opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--pointer": case "--pointer":
opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required")) opts.Pointer = firstLine(nextString(allArgs, &i, "pointer sign string required"))
validatePointer = true
case "--marker": case "--marker":
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required")) opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
validateMarker = true
case "--sync": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync": case "--no-sync":
@ -1845,6 +1843,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Unicode = false opts.Unicode = false
case "--unicode": case "--unicode":
opts.Unicode = true opts.Unicode = true
case "--ambidouble":
opts.Ambidouble = true
case "--no-ambidouble":
opts.Ambidouble = false
case "--margin": case "--margin":
opts.Margin = parseMargin( opts.Margin = parseMargin(
"margin", "margin",
@ -1903,10 +1905,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "--pointer="); match { } else if match, value := optString(arg, "--pointer="); match {
opts.Pointer = firstLine(value) opts.Pointer = firstLine(value)
validatePointer = true
} else if match, value := optString(arg, "--marker="); match { } else if match, value := optString(arg, "--marker="); match {
opts.Marker = firstLine(value) opts.Marker = firstLine(value)
validateMarker = true
} else if match, value := optString(arg, "-n", "--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value) opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match { } 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 { func validateSign(sign string, signOptName string) error {
if sign == "" { if sign == "" {
return fmt.Errorf("%v cannot be empty", signOptName) 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 fmt.Errorf("%v display width should be up to 2", signOptName)
} }
return nil return nil
} }
func postProcessOptions(opts *Options) { 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 { if !opts.Version && !tui.IsLightRendererSupported() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on this platform") 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") errorExit("--scrollbar should be given one or two characters")
} }
for _, r := range runes { for _, r := range runes {
if runewidth.RuneWidth(r) != 1 { if uniseg.StringWidth(string(r)) != 1 {
errorExit("scrollbar display width should be 1") errorExit("scrollbar display width should be 1")
} }
} }

View File

@ -17,8 +17,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/junegunn/go-runewidth" "github.com/junegunn/uniseg"
"github.com/rivo/uniseg"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util" "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) t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
} }
if t.unicode { if t.unicode {
t.borderWidth = runewidth.RuneWidth('│') t.borderWidth = uniseg.StringWidth("│")
} }
if opts.Scrollbar == nil { if opts.Scrollbar == nil {
if t.unicode && t.borderWidth == 1 { if t.unicode && t.borderWidth == 1 {

View File

@ -10,8 +10,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/go-runewidth" "github.com/junegunn/uniseg"
"github.com/rivo/uniseg"
"golang.org/x/term" "golang.org/x/term"
) )
@ -804,7 +803,7 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.top) hw := runeWidth(w.border.top)
if top { if top {
w.Move(0, 0) w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw)) w.CPrint(color, repeat(w.border.top, w.width/hw))
@ -842,13 +841,13 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
if w.preview { if w.preview {
color = ColPreviewBorder color = ColPreviewBorder
} }
hw := runewidth.RuneWidth(w.border.top) hw := runeWidth(w.border.top)
tcw := runewidth.RuneWidth(w.border.topLeft) + runewidth.RuneWidth(w.border.topRight) tcw := runeWidth(w.border.topLeft) + runeWidth(w.border.topRight)
bcw := runewidth.RuneWidth(w.border.bottomLeft) + runewidth.RuneWidth(w.border.bottomRight) bcw := runeWidth(w.border.bottomLeft) + runeWidth(w.border.bottomRight)
rem := (w.width - tcw) % hw 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)) w.CPrint(color, string(w.border.topLeft)+repeat(w.border.top, (w.width-tcw)/hw)+repeat(' ', rem)+string(w.border.topRight))
if !onlyHorizontal { if !onlyHorizontal {
vw := runewidth.RuneWidth(w.border.left) vw := runeWidth(w.border.left)
for y := 1; y < w.height-1; y++ { for y := 1; y < w.height-1; y++ {
w.Move(y, 0) w.Move(y, 0)
w.CPrint(color, string(w.border.left)) 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' { } else if rs[0] == '\r' {
w++ w++
} else { } else {
w = runewidth.StringWidth(str) w = uniseg.StringWidth(str)
} }
width += w width += w

View File

@ -10,8 +10,7 @@ import (
"github.com/gdamore/tcell/v2/encoding" "github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-runewidth" "github.com/junegunn/uniseg"
"github.com/rivo/uniseg"
) )
func HasFullscreenRenderer() bool { func HasFullscreenRenderer() bool {
@ -738,7 +737,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
style = w.normal.style() style = w.normal.style()
} }
hw := runewidth.RuneWidth(w.borderStyle.top) hw := runeWidth(w.borderStyle.top)
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
max := right - 2*hw max := right - 2*hw
@ -773,7 +772,7 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
} }
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight: 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++ { for y := top; y < bot; y++ {
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style) _screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
} }
@ -782,8 +781,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
switch shape { switch shape {
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble: case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style) _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(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)
} }
} }

View File

@ -5,6 +5,8 @@ import (
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/junegunn/uniseg"
) )
// Types of user action // Types of user action
@ -812,3 +814,7 @@ func initPalette(theme *ColorTheme) {
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg) ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg) ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
} }
func runeWidth(r rune) int {
return uniseg.StringWidth(string(r))
}

View File

@ -6,14 +6,13 @@ import (
"strings" "strings"
"time" "time"
"github.com/junegunn/go-runewidth" "github.com/junegunn/uniseg"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/rivo/uniseg"
) )
// StringWidth returns string width where each CR/LF character takes 1 column // StringWidth returns string width where each CR/LF character takes 1 column
func StringWidth(s string) int { 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 // RunesWidth returns runes width
@ -165,7 +164,7 @@ func RepeatToFill(str string, length int, limit int) string {
output := strings.Repeat(str, times) output := strings.Repeat(str, times)
if rest > 0 { if rest > 0 {
for _, r := range str { for _, r := range str {
rest -= runewidth.RuneWidth(r) rest -= uniseg.StringWidth(string(r))
if rest < 0 { if rest < 0 {
break break
} }