| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- package ansi
- import (
- "bytes"
- "github.com/charmbracelet/x/ansi/parser"
- "github.com/rivo/uniseg"
- )
- // Truncate truncates a string to a given length, adding a tail to the
- // end if the string is longer than the given length.
- // This function is aware of ANSI escape codes and will not break them, and
- // accounts for wide-characters (such as East Asians and emojis).
- func Truncate(s string, length int, tail string) string {
- if sw := StringWidth(s); sw <= length {
- return s
- }
- tw := StringWidth(tail)
- length -= tw
- if length < 0 {
- return ""
- }
- var cluster []byte
- var buf bytes.Buffer
- curWidth := 0
- ignoring := false
- pstate := parser.GroundState // initial state
- b := []byte(s)
- i := 0
- // Here we iterate over the bytes of the string and collect printable
- // characters and runes. We also keep track of the width of the string
- // in cells.
- // Once we reach the given length, we start ignoring characters and only
- // collect ANSI escape codes until we reach the end of string.
- for i < len(b) {
- state, action := parser.Table.Transition(pstate, b[i])
- if state == parser.Utf8State {
- // This action happens when we transition to the Utf8State.
- var width int
- cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
- // increment the index by the length of the cluster
- i += len(cluster)
- // Are we ignoring? Skip to the next byte
- if ignoring {
- continue
- }
- // Is this gonna be too wide?
- // If so write the tail and stop collecting.
- if curWidth+width > length && !ignoring {
- ignoring = true
- buf.WriteString(tail)
- }
- if curWidth+width > length {
- continue
- }
- curWidth += width
- buf.Write(cluster)
- // Done collecting, now we're back in the ground state.
- pstate = parser.GroundState
- continue
- }
- switch action {
- case parser.PrintAction:
- // Is this gonna be too wide?
- // If so write the tail and stop collecting.
- if curWidth >= length && !ignoring {
- ignoring = true
- buf.WriteString(tail)
- }
- // Skip to the next byte if we're ignoring
- if ignoring {
- i++
- continue
- }
- // collects printable ASCII
- curWidth++
- fallthrough
- default:
- buf.WriteByte(b[i])
- i++
- }
- // Transition to the next state.
- pstate = state
- // Once we reach the given length, we start ignoring runes and write
- // the tail to the buffer.
- if curWidth > length && !ignoring {
- ignoring = true
- buf.WriteString(tail)
- }
- }
- return buf.String()
- }
- // TruncateLeft truncates a string from the left side to a given length, adding
- // a prefix to the beginning if the string is longer than the given length.
- // This function is aware of ANSI escape codes and will not break them, and
- // accounts for wide-characters (such as East Asians and emojis).
- func TruncateLeft(s string, length int, prefix string) string {
- if length == 0 {
- return ""
- }
- var cluster []byte
- var buf bytes.Buffer
- curWidth := 0
- ignoring := true
- pstate := parser.GroundState
- b := []byte(s)
- i := 0
- for i < len(b) {
- if !ignoring {
- buf.Write(b[i:])
- break
- }
- state, action := parser.Table.Transition(pstate, b[i])
- if state == parser.Utf8State {
- var width int
- cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
- i += len(cluster)
- curWidth += width
- if curWidth > length && ignoring {
- ignoring = false
- buf.WriteString(prefix)
- }
- if ignoring {
- continue
- }
- if curWidth > length {
- buf.Write(cluster)
- }
- pstate = parser.GroundState
- continue
- }
- switch action {
- case parser.PrintAction:
- curWidth++
- if curWidth > length && ignoring {
- ignoring = false
- buf.WriteString(prefix)
- }
- if ignoring {
- i++
- continue
- }
- fallthrough
- default:
- buf.WriteByte(b[i])
- i++
- }
- pstate = state
- if curWidth > length && ignoring {
- ignoring = false
- buf.WriteString(prefix)
- }
- }
- return buf.String()
- }
|