decentral1se 1723025fbf
build: go 1.24
We were running behind and there were quite some deprecations to update.
This was mostly in the upstream copy/pasta package but seems quite
minimal.
2025-03-16 12:31:45 +01:00

186 lines
3.6 KiB
Go

package cellbuf
import (
"bytes"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/x/ansi"
)
const nbsp = '\u00a0'
// Wrap returns a string that is wrapped to the specified limit applying any
// ANSI escape sequences in the string. It tries to wrap the string at word
// boundaries, but will break words if necessary.
//
// The breakpoints string is a list of characters that are considered
// breakpoints for word wrapping. A hyphen (-) is always considered a
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func Wrap(s string, limit int, breakpoints string) string {
if len(s) == 0 {
return ""
}
if limit < 1 {
return s
}
p := ansi.GetParser()
defer ansi.PutParser(p)
var (
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
style, curStyle Style
link, curLink Link
curWidth int
wordLen int
)
hasBlankStyle := func() bool {
// Only follow reverse attribute, bg color and underline style
return !style.Attrs.Contains(ReverseAttr) && style.Bg == nil && style.UlStyle == NoUnderline
}
addSpace := func() {
curWidth += space.Len()
buf.Write(space.Bytes())
space.Reset()
}
addWord := func() {
if word.Len() == 0 {
return
}
curLink = link
curStyle = style
addSpace()
curWidth += wordLen
buf.Write(word.Bytes())
word.Reset()
wordLen = 0
}
addNewline := func() {
if !curStyle.Empty() {
buf.WriteString(ansi.ResetStyle)
}
if !curLink.Empty() {
buf.WriteString(ansi.ResetHyperlink())
}
buf.WriteByte('\n')
if !curLink.Empty() {
buf.WriteString(ansi.SetHyperlink(curLink.URL, curLink.Params))
}
if !curStyle.Empty() {
buf.WriteString(curStyle.Sequence())
}
curWidth = 0
space.Reset()
}
var state byte
for len(s) > 0 {
seq, width, n, newState := ansi.DecodeSequence(s, state, p)
switch width {
case 0:
if ansi.Equal(seq, "\t") {
addWord()
space.WriteString(seq)
break
} else if ansi.Equal(seq, "\n") {
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
addNewline()
break
} else if ansi.HasCsiPrefix(seq) && p.Command() == 'm' {
// SGR style sequence [ansi.SGR]
ReadStyle(p.Params(), &style)
} else if ansi.HasOscPrefix(seq) && p.Command() == 8 {
// Hyperlink sequence [ansi.SetHyperlink]
ReadLink(p.Data(), &link)
}
word.WriteString(seq)
default:
if len(seq) == 1 {
// ASCII
r, _ := utf8.DecodeRuneInString(seq)
if r != nbsp && unicode.IsSpace(r) && hasBlankStyle() {
addWord()
space.WriteRune(r)
break
} else if r == '-' || runeContainsAny(r, breakpoints) {
addSpace()
if curWidth+wordLen+width <= limit {
addWord()
buf.WriteString(seq)
curWidth += width
break
}
}
}
if wordLen+width > limit {
// Hardwrap the word if it's too long
addWord()
}
word.WriteString(seq)
wordLen += width
if curWidth+wordLen+space.Len() > limit {
addNewline()
}
}
s = s[n:]
state = newState
}
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
if !curLink.Empty() {
buf.WriteString(ansi.ResetHyperlink())
}
if !curStyle.Empty() {
buf.WriteString(ansi.ResetStyle)
}
return buf.String()
}
func runeContainsAny[T string | []rune](r rune, s T) bool {
for _, c := range []rune(s) {
if c == r {
return true
}
}
return false
}