width.go 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. package ansi
  2. import (
  3. "bytes"
  4. "github.com/charmbracelet/x/ansi/parser"
  5. "github.com/rivo/uniseg"
  6. )
  7. // Strip removes ANSI escape codes from a string.
  8. func Strip(s string) string {
  9. var (
  10. buf bytes.Buffer // buffer for collecting printable characters
  11. ri int // rune index
  12. rw int // rune width
  13. pstate = parser.GroundState // initial state
  14. )
  15. // This implements a subset of the Parser to only collect runes and
  16. // printable characters.
  17. for i := 0; i < len(s); i++ {
  18. var state, action byte
  19. if pstate != parser.Utf8State {
  20. state, action = parser.Table.Transition(pstate, s[i])
  21. }
  22. switch {
  23. case pstate == parser.Utf8State:
  24. // During this state, collect rw bytes to form a valid rune in the
  25. // buffer. After getting all the rune bytes into the buffer,
  26. // transition to GroundState and reset the counters.
  27. buf.WriteByte(s[i])
  28. ri++
  29. if ri < rw {
  30. continue
  31. }
  32. pstate = parser.GroundState
  33. ri = 0
  34. rw = 0
  35. case action == parser.PrintAction:
  36. // This action happens when we transition to the Utf8State.
  37. if w := utf8ByteLen(s[i]); w > 1 {
  38. rw = w
  39. buf.WriteByte(s[i])
  40. ri++
  41. break
  42. }
  43. fallthrough
  44. case action == parser.ExecuteAction:
  45. // collects printable ASCII and non-printable characters
  46. buf.WriteByte(s[i])
  47. }
  48. // Transition to the next state.
  49. // The Utf8State is managed separately above.
  50. if pstate != parser.Utf8State {
  51. pstate = state
  52. }
  53. }
  54. return buf.String()
  55. }
  56. // StringWidth returns the width of a string in cells. This is the number of
  57. // cells that the string will occupy when printed in a terminal. ANSI escape
  58. // codes are ignored and wide characters (such as East Asians and emojis) are
  59. // accounted for.
  60. func StringWidth(s string) int {
  61. if s == "" {
  62. return 0
  63. }
  64. var (
  65. gstate = -1
  66. pstate = parser.GroundState // initial state
  67. cluster string
  68. width int
  69. )
  70. for i := 0; i < len(s); i++ {
  71. state, action := parser.Table.Transition(pstate, s[i])
  72. switch action {
  73. case parser.PrintAction:
  74. if utf8ByteLen(s[i]) > 1 {
  75. var w int
  76. cluster, _, w, gstate = uniseg.FirstGraphemeClusterInString(s[i:], gstate)
  77. width += w
  78. i += len(cluster) - 1
  79. pstate = parser.GroundState
  80. continue
  81. }
  82. width++
  83. fallthrough
  84. default:
  85. // Reset uniseg state when we're not in a printable state.
  86. gstate = -1
  87. }
  88. pstate = state
  89. }
  90. return width
  91. }