diff --git a/go.mod b/go.mod index be1ff7e0..5f4e718f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/ansi v0.11.7 + github.com/charmbracelet/x/ansi v0.11.8 github.com/jessevdk/go-flags v1.6.1 github.com/mattn/go-runewidth v0.0.27 github.com/muesli/termenv v0.16.0 diff --git a/go.sum b/go.sum index 8e3a6ebb..bba6c447 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= -github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= +github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= diff --git a/vendor/github.com/charmbracelet/x/ansi/color.go b/vendor/github.com/charmbracelet/x/ansi/color.go index 09feb975..eacca595 100644 --- a/vendor/github.com/charmbracelet/x/ansi/color.go +++ b/vendor/github.com/charmbracelet/x/ansi/color.go @@ -236,7 +236,7 @@ func Convert256(c color.Color) IndexedColor { if colorDist <= grayDist { return IndexedColor(16 + ci) //nolint:gosec } - return IndexedColor(232 + greyIdx) //nolint:gosec + return IndexedColor(232 + greyIdx) // // Is grey or 6x6x6 color closest? // d := distSq(cr, cg, cb, int(r), int(g), int(b)) diff --git a/vendor/github.com/charmbracelet/x/ansi/mouse.go b/vendor/github.com/charmbracelet/x/ansi/mouse.go index 94b8b485..bac4620f 100644 --- a/vendor/github.com/charmbracelet/x/ansi/mouse.go +++ b/vendor/github.com/charmbracelet/x/ansi/mouse.go @@ -148,7 +148,7 @@ const x10Offset = 32 // // See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking func MouseX10(b byte, x, y int) string { - return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1) + return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1) //nolint:gosec } // MouseSgr returns an escape sequence representing a mouse event in SGR mode. diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go index ef46b7b6..14daaf51 100644 --- a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go +++ b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go @@ -52,7 +52,7 @@ func (t TransitionTable) AddMany(codes []byte, state State, action Action, next // AddRange adds a range of transitions. func (t TransitionTable) AddRange(start, end byte, state State, action Action, next State) { for i := int(start); i <= int(end); i++ { - t.AddOne(byte(i), state, action, next) + t.AddOne(byte(i), state, action, next) //nolint:gosec } } @@ -67,7 +67,7 @@ func (t TransitionTable) Transition(state State, code byte) (State, Action) { func r(start, end byte) []byte { var a []byte for i := int(start); i <= int(end); i++ { - a = append(a, byte(i)) + a = append(a, byte(i)) //nolint:gosec } return a } diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go index 24a8f8c5..486135b7 100644 --- a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go +++ b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go @@ -437,13 +437,13 @@ func FirstGraphemeCluster[T string | []byte](b T, m Method) (T, int) { case string: cluster := graphemes.FromString(b).First() if m == WcWidth { - return T(cluster), wcOptions.StringWidth(cluster) + return T(cluster), wcClusterWidth(cluster) } return T(cluster), dwOptions.String(cluster) case []byte: cluster := graphemes.FromBytes(b).First() if m == WcWidth { - return T(cluster), wcOptions.StringWidth(string(cluster)) + return T(cluster), wcClusterWidth(cluster) } return T(cluster), dwOptions.Bytes(cluster) } @@ -460,7 +460,7 @@ type Cmd int // range of 0x3C-0x3F. // Zero is returned if the sequence does not have a prefix. func (c Cmd) Prefix() byte { - return byte(parser.Prefix(int(c))) + return byte(parser.Prefix(int(c))) //nolint:gosec } // Intermediate returns the unpacked intermediate byte of the CSI sequence. @@ -469,12 +469,12 @@ func (c Cmd) Prefix() byte { // ',', '-', '.', '/'. // Zero is returned if the sequence does not have an intermediate byte. func (c Cmd) Intermediate() byte { - return byte(parser.Intermediate(int(c))) + return byte(parser.Intermediate(int(c))) //nolint:gosec } // Final returns the unpacked command byte of the CSI sequence. func (c Cmd) Final() byte { - return byte(parser.Command(int(c))) + return byte(parser.Command(int(c))) //nolint:gosec } // Command packs a command with the given prefix, intermediate, and final. A diff --git a/vendor/github.com/charmbracelet/x/ansi/wcwidth.go b/vendor/github.com/charmbracelet/x/ansi/wcwidth.go new file mode 100644 index 00000000..642a4f6f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/wcwidth.go @@ -0,0 +1,100 @@ +package ansi + +import ( + "slices" + "unicode" + "unicode/utf8" +) + +// zeroWidthTables are the categories whose runes don't advance the cursor: +// nonspacing and enclosing marks, and format characters such as ZWJ and the +// variation selectors. +var zeroWidthTables = []*unicode.RangeTable{unicode.Mn, unicode.Me, unicode.Cf} + +// zeroWidthBMP is a bitset of the [zeroWidthTables] runes in the basic +// multilingual plane. Measuring runs a lookup per rune, and a bit test is a +// good deal cheaper than searching three range tables. Runes above the BMP are +// rare enough in terminal output to leave on the slow path. +var zeroWidthBMP [0x10000 / 64]uint64 + +// zeroWidthHigh holds the [zeroWidthTables] runes above the BMP, expanded and +// sorted so a lookup is one binary search. +var zeroWidthHigh []rune + +func init() { + for _, t := range zeroWidthTables { + for _, r := range t.R16 { + for c := rune(r.Lo); c <= rune(r.Hi); c += rune(r.Stride) { + zeroWidthBMP[c>>6] |= 1 << (c & 63) + } + } + for _, r := range t.R32 { + // R32 range bounds are always valid runes. + for c := rune(r.Lo); c <= rune(r.Hi); c += rune(r.Stride) { //nolint:gosec + zeroWidthHigh = append(zeroWidthHigh, c) + } + } + } + slices.Sort(zeroWidthHigh) +} + +// isZeroWidthHigh reports whether a rune above the BMP is zero width. +func isZeroWidthHigh(r rune) bool { + _, ok := slices.BinarySearch(zeroWidthHigh, r) + return ok +} + +// wcRuneWidth returns the number of columns a single rune advances the cursor, +// i.e. wcwidth(3). +// +// Nonspacing and enclosing marks, along with format characters such as ZWJ and +// the variation selectors, never advance the cursor. [runewidth] reports some +// of them, most notably the Indic combining marks, as single width, so they're +// classified here instead. +func wcRuneWidth(r rune) int { + switch { + case r < 0x20, r >= 0x7f && r < 0xa0: + // C0 and C1 controls, and DEL, don't print. + return 0 + case r < 0x7f: + // Printable ASCII. Everything above it can be East Asian Ambiguous, + // so it has to go through runewidth. + return 1 + case r < 0x10000: + if zeroWidthBMP[r>>6]&(1<<(r&63)) != 0 { + return 0 + } + case isZeroWidthHigh(r): + return 0 + } + return wcOptions.RuneWidth(r) +} + +// wcClusterWidth returns the number of columns a grapheme cluster occupies +// under [WcWidth], which is the sum of the widths of its codepoints. +// +// Terminals that don't implement Unicode core mode (DEC mode 2027) advance the +// cursor once per codepoint and know nothing about cluster boundaries. So a +// cluster can be wider than the glyph it draws: Unicode 15.1 merged Indic +// conjuncts into single clusters, and a terminal still puts "स्ते" in two +// columns, one per consonant. ZWJ emoji sequences behave the same way, with +// "👨‍👩‍👧‍👦" taking the eight columns of the four emoji it's built from. +// +// Measuring the cluster as a unit is [GraphemeWidth]'s job. +func wcClusterWidth[T string | []byte](cluster T) int { + var width int + switch c := any(cluster).(type) { + case string: + for _, r := range c { + width += wcRuneWidth(r) + } + case []byte: + // Ranging over string(c) would copy the slice, so decode in place. + for len(c) > 0 { + r, size := utf8.DecodeRune(c) + width += wcRuneWidth(r) + c = c[size:] + } + } + return width +} diff --git a/vendor/github.com/charmbracelet/x/ansi/width.go b/vendor/github.com/charmbracelet/x/ansi/width.go index a4735b8a..30c7caa0 100644 --- a/vendor/github.com/charmbracelet/x/ansi/width.go +++ b/vendor/github.com/charmbracelet/x/ansi/width.go @@ -2,6 +2,7 @@ package ansi import ( "bytes" + "unicode/utf8" "github.com/charmbracelet/x/ansi/parser" ) @@ -88,6 +89,17 @@ func stringWidth(m Method, s string) int { for i := 0; i < len(s); i++ { state, action := parser.Table.Transition(pstate, s[i]) if action == parser.PrintAction || state == parser.Utf8State { + if m == WcWidth { + // Wcwidth sums codepoints, so where the cluster boundaries + // fall makes no difference to the total. Skipping the + // segmentation is a good deal cheaper. + r, size := utf8.DecodeRuneInString(s[i:]) + width += wcRuneWidth(r) + i += size - 1 + pstate = parser.GroundState + continue + } + cluster, w := FirstGraphemeCluster(s[i:], m) width += w diff --git a/vendor/modules.txt b/vendor/modules.txt index 9aac6dc8..1733057d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -25,7 +25,7 @@ github.com/charmbracelet/colorprofile # github.com/charmbracelet/lipgloss v1.1.0 ## explicit; go 1.18 github.com/charmbracelet/lipgloss -# github.com/charmbracelet/x/ansi v0.11.7 +# github.com/charmbracelet/x/ansi v0.11.8 ## explicit; go 1.24.2 github.com/charmbracelet/x/ansi github.com/charmbracelet/x/ansi/parser