truncate.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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. gstate := -1
  25. pstate := parser.GroundState // initial state
  26. b := []byte(s)
  27. i := 0
  28. // Here we iterate over the bytes of the string and collect printable
  29. // characters and runes. We also keep track of the width of the string
  30. // in cells.
  31. // Once we reach the given length, we start ignoring characters and only
  32. // collect ANSI escape codes until we reach the end of string.
  33. for i < len(b) {
  34. state, action := parser.Table.Transition(pstate, b[i])
  35. switch action {
  36. case parser.PrintAction:
  37. if utf8ByteLen(b[i]) > 1 {
  38. // This action happens when we transition to the Utf8State.
  39. var width int
  40. cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
  41. // increment the index by the length of the cluster
  42. i += len(cluster)
  43. // Are we ignoring? Skip to the next byte
  44. if ignoring {
  45. continue
  46. }
  47. // Is this gonna be too wide?
  48. // If so write the tail and stop collecting.
  49. if curWidth+width > length && !ignoring {
  50. ignoring = true
  51. buf.WriteString(tail)
  52. }
  53. if curWidth+width > length {
  54. continue
  55. }
  56. curWidth += width
  57. for _, r := range cluster {
  58. buf.WriteByte(r)
  59. }
  60. gstate = -1 // reset grapheme state otherwise, width calculation might be off
  61. // Done collecting, now we're back in the ground state.
  62. pstate = parser.GroundState
  63. continue
  64. }
  65. // Is this gonna be too wide?
  66. // If so write the tail and stop collecting.
  67. if curWidth >= length && !ignoring {
  68. ignoring = true
  69. buf.WriteString(tail)
  70. }
  71. // Skip to the next byte if we're ignoring
  72. if ignoring {
  73. i++
  74. continue
  75. }
  76. // collects printable ASCII
  77. curWidth++
  78. fallthrough
  79. default:
  80. buf.WriteByte(b[i])
  81. i++
  82. }
  83. // Transition to the next state.
  84. pstate = state
  85. // Once we reach the given length, we start ignoring runes and write
  86. // the tail to the buffer.
  87. if curWidth > length && !ignoring {
  88. ignoring = true
  89. buf.WriteString(tail)
  90. }
  91. }
  92. return buf.String()
  93. }