truncate.go 2.5 KB

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