truncate.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. package ansi
  2. import (
  3. "bytes"
  4. "github.com/charmbracelet/x/ansi/parser"
  5. "github.com/rivo/uniseg"
  6. )
  7. // Truncate truncates a string to a given length, adding a tail to the
  8. // end if the string is longer than the given length.
  9. // This function is aware of ANSI escape codes and will not break them, and
  10. // accounts for wide-characters (such as East Asians and emojis).
  11. func Truncate(s string, length int, tail string) string {
  12. if sw := StringWidth(s); sw <= length {
  13. return s
  14. }
  15. tw := StringWidth(tail)
  16. length -= tw
  17. if length < 0 {
  18. return ""
  19. }
  20. var cluster []byte
  21. var buf bytes.Buffer
  22. curWidth := 0
  23. ignoring := false
  24. pstate := parser.GroundState // initial state
  25. b := []byte(s)
  26. i := 0
  27. // Here we iterate over the bytes of the string and collect printable
  28. // characters and runes. We also keep track of the width of the string
  29. // in cells.
  30. // Once we reach the given length, we start ignoring characters and only
  31. // collect ANSI escape codes until we reach the end of string.
  32. for i < len(b) {
  33. state, action := parser.Table.Transition(pstate, b[i])
  34. if state == parser.Utf8State {
  35. // This action happens when we transition to the Utf8State.
  36. var width int
  37. cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
  38. // increment the index by the length of the cluster
  39. i += len(cluster)
  40. // Are we ignoring? Skip to the next byte
  41. if ignoring {
  42. continue
  43. }
  44. // Is this gonna be too wide?
  45. // If so write the tail and stop collecting.
  46. if curWidth+width > length && !ignoring {
  47. ignoring = true
  48. buf.WriteString(tail)
  49. }
  50. if curWidth+width > length {
  51. continue
  52. }
  53. curWidth += width
  54. buf.Write(cluster)
  55. // Done collecting, now we're back in the ground state.
  56. pstate = parser.GroundState
  57. continue
  58. }
  59. switch action {
  60. case parser.PrintAction:
  61. // Is this gonna be too wide?
  62. // If so write the tail and stop collecting.
  63. if curWidth >= length && !ignoring {
  64. ignoring = true
  65. buf.WriteString(tail)
  66. }
  67. // Skip to the next byte if we're ignoring
  68. if ignoring {
  69. i++
  70. continue
  71. }
  72. // collects printable ASCII
  73. curWidth++
  74. fallthrough
  75. default:
  76. buf.WriteByte(b[i])
  77. i++
  78. }
  79. // Transition to the next state.
  80. pstate = state
  81. // Once we reach the given length, we start ignoring runes and write
  82. // the tail to the buffer.
  83. if curWidth > length && !ignoring {
  84. ignoring = true
  85. buf.WriteString(tail)
  86. }
  87. }
  88. return buf.String()
  89. }
  90. // TruncateLeft truncates a string from the left side to a given length, adding
  91. // a prefix to the beginning if the string is longer than the given length.
  92. // This function is aware of ANSI escape codes and will not break them, and
  93. // accounts for wide-characters (such as East Asians and emojis).
  94. func TruncateLeft(s string, length int, prefix string) string {
  95. if length == 0 {
  96. return ""
  97. }
  98. var cluster []byte
  99. var buf bytes.Buffer
  100. curWidth := 0
  101. ignoring := true
  102. pstate := parser.GroundState
  103. b := []byte(s)
  104. i := 0
  105. for i < len(b) {
  106. if !ignoring {
  107. buf.Write(b[i:])
  108. break
  109. }
  110. state, action := parser.Table.Transition(pstate, b[i])
  111. if state == parser.Utf8State {
  112. var width int
  113. cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
  114. i += len(cluster)
  115. curWidth += width
  116. if curWidth > length && ignoring {
  117. ignoring = false
  118. buf.WriteString(prefix)
  119. }
  120. if ignoring {
  121. continue
  122. }
  123. if curWidth > length {
  124. buf.Write(cluster)
  125. }
  126. pstate = parser.GroundState
  127. continue
  128. }
  129. switch action {
  130. case parser.PrintAction:
  131. curWidth++
  132. if curWidth > length && ignoring {
  133. ignoring = false
  134. buf.WriteString(prefix)
  135. }
  136. if ignoring {
  137. i++
  138. continue
  139. }
  140. fallthrough
  141. default:
  142. buf.WriteByte(b[i])
  143. i++
  144. }
  145. pstate = state
  146. if curWidth > length && ignoring {
  147. ignoring = false
  148. buf.WriteString(prefix)
  149. }
  150. }
  151. return buf.String()
  152. }