150 lines
4.9 KiB
Go
150 lines
4.9 KiB
Go
package displaywidth
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/clipperhouse/uax29/v2/graphemes"
|
|
)
|
|
|
|
// TruncateString truncates a string to the given maxWidth, and appends the
|
|
// given tail if the string is truncated.
|
|
//
|
|
// It ensures the visible width, including the width of the tail, is less than or
|
|
// equal to maxWidth.
|
|
//
|
|
// When [Options.ControlSequences] is true, 7-bit ANSI escape sequences that
|
|
// appear after the truncation point are preserved in the output. This ensures
|
|
// that escape sequences such as SGR resets are not lost, preventing color
|
|
// bleed in terminal output.
|
|
//
|
|
// [Options.ControlSequences8Bit] is ignored by truncation. 8-bit C1 byte values
|
|
// (0x80-0x9F) overlap with UTF-8 multi-byte encoding, so manipulating them
|
|
// during truncation can shift byte boundaries and form unintended visible
|
|
// characters. Use [Options.String] or [Options.Bytes] for 8-bit-aware width
|
|
// measurement.
|
|
func (options Options) TruncateString(s string, maxWidth int, tail string) string {
|
|
// We deliberately ignore ControlSequences8Bit for truncation, see above.
|
|
options.ControlSequences8Bit = false
|
|
|
|
maxWidthWithoutTail := maxWidth - options.String(tail)
|
|
|
|
var pos, total int
|
|
g := graphemes.FromString(s)
|
|
g.AnsiEscapeSequences = options.ControlSequences
|
|
|
|
for g.Next() {
|
|
gw := graphemeWidth(g.Value(), options)
|
|
if total+gw <= maxWidthWithoutTail {
|
|
pos = g.End()
|
|
}
|
|
total += gw
|
|
if total > maxWidth {
|
|
if options.ControlSequences {
|
|
// Build result with trailing 7-bit ANSI escape sequences preserved
|
|
var b strings.Builder
|
|
b.Grow(len(s) + len(tail)) // at most original + tail
|
|
b.WriteString(s[:pos])
|
|
b.WriteString(tail)
|
|
|
|
rem := graphemes.FromString(s[pos:])
|
|
rem.AnsiEscapeSequences = options.ControlSequences
|
|
|
|
for rem.Next() {
|
|
v := rem.Value()
|
|
// Only preserve 7-bit escapes (ESC = 0x1B) that measure
|
|
// as zero-width on their own; some sequences (e.g. SOS)
|
|
// are only valid in their original context.
|
|
if len(v) > 0 && v[0] == 0x1B && options.String(v) == 0 {
|
|
b.WriteString(v)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
return s[:pos] + tail
|
|
}
|
|
}
|
|
// No truncation
|
|
return s
|
|
}
|
|
|
|
// TruncateString truncates a string to the given maxWidth, and appends the
|
|
// given tail if the string is truncated.
|
|
//
|
|
// It ensures the total width, including the width of the tail, is less than or
|
|
// equal to maxWidth.
|
|
func TruncateString(s string, maxWidth int, tail string) string {
|
|
return DefaultOptions.TruncateString(s, maxWidth, tail)
|
|
}
|
|
|
|
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
|
|
// given tail if the []byte is truncated.
|
|
//
|
|
// It ensures the visible width, including the width of the tail, is less than or
|
|
// equal to maxWidth.
|
|
//
|
|
// When [Options.ControlSequences] is true, 7-bit ANSI escape sequences that
|
|
// appear after the truncation point are preserved in the output. This ensures
|
|
// that escape sequences such as SGR resets are not lost, preventing color
|
|
// bleed in terminal output.
|
|
//
|
|
// [Options.ControlSequences8Bit] is ignored by truncation. 8-bit C1 byte values
|
|
// (0x80-0x9F) overlap with UTF-8 multi-byte encoding, so manipulating them
|
|
// during truncation can shift byte boundaries and form unintended visible
|
|
// characters. Use [Options.String] or [Options.Bytes] for 8-bit-aware width
|
|
// measurement.
|
|
func (options Options) TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
|
|
// We deliberately ignore ControlSequences8Bit for truncation, see above.
|
|
options.ControlSequences8Bit = false
|
|
|
|
maxWidthWithoutTail := maxWidth - options.Bytes(tail)
|
|
|
|
var pos, total int
|
|
g := graphemes.FromBytes(s)
|
|
g.AnsiEscapeSequences = options.ControlSequences
|
|
|
|
for g.Next() {
|
|
gw := graphemeWidth(g.Value(), options)
|
|
if total+gw <= maxWidthWithoutTail {
|
|
pos = g.End()
|
|
}
|
|
total += gw
|
|
if total > maxWidth {
|
|
if options.ControlSequences {
|
|
// Build result with trailing 7-bit ANSI escape sequences preserved
|
|
result := make([]byte, 0, len(s)+len(tail)) // at most original + tail
|
|
result = append(result, s[:pos]...)
|
|
result = append(result, tail...)
|
|
|
|
rem := graphemes.FromBytes(s[pos:])
|
|
rem.AnsiEscapeSequences = options.ControlSequences
|
|
|
|
for rem.Next() {
|
|
v := rem.Value()
|
|
// Only preserve 7-bit escapes (ESC = 0x1B) that measure
|
|
// as zero-width on their own; some sequences (e.g. SOS)
|
|
// are only valid in their original context.
|
|
if len(v) > 0 && v[0] == 0x1B && options.Bytes(v) == 0 {
|
|
result = append(result, v...)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
result := make([]byte, 0, pos+len(tail))
|
|
result = append(result, s[:pos]...)
|
|
result = append(result, tail...)
|
|
return result
|
|
}
|
|
}
|
|
// No truncation
|
|
return s
|
|
}
|
|
|
|
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
|
|
// given tail if the []byte is truncated.
|
|
//
|
|
// It ensures the total width, including the width of the tail, is less than or
|
|
// equal to maxWidth.
|
|
func TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
|
|
return DefaultOptions.TruncateBytes(s, maxWidth, tail)
|
|
}
|