width.go 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  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. if pstate == parser.Utf8State {
  19. // During this state, collect rw bytes to form a valid rune in the
  20. // buffer. After getting all the rune bytes into the buffer,
  21. // transition to GroundState and reset the counters.
  22. buf.WriteByte(s[i])
  23. ri++
  24. if ri < rw {
  25. continue
  26. }
  27. pstate = parser.GroundState
  28. ri = 0
  29. rw = 0
  30. continue
  31. }
  32. state, action := parser.Table.Transition(pstate, s[i])
  33. switch action {
  34. case parser.CollectAction:
  35. if state == parser.Utf8State {
  36. // This action happens when we transition to the Utf8State.
  37. rw = utf8ByteLen(s[i])
  38. buf.WriteByte(s[i])
  39. ri++
  40. }
  41. case parser.PrintAction, parser.ExecuteAction:
  42. // collects printable ASCII and non-printable characters
  43. buf.WriteByte(s[i])
  44. }
  45. // Transition to the next state.
  46. // The Utf8State is managed separately above.
  47. if pstate != parser.Utf8State {
  48. pstate = state
  49. }
  50. }
  51. return buf.String()
  52. }
  53. // StringWidth returns the width of a string in cells. This is the number of
  54. // cells that the string will occupy when printed in a terminal. ANSI escape
  55. // codes are ignored and wide characters (such as East Asians and emojis) are
  56. // accounted for.
  57. func StringWidth(s string) int {
  58. if s == "" {
  59. return 0
  60. }
  61. var (
  62. pstate = parser.GroundState // initial state
  63. cluster string
  64. width int
  65. )
  66. for i := 0; i < len(s); i++ {
  67. state, action := parser.Table.Transition(pstate, s[i])
  68. if state == parser.Utf8State {
  69. var w int
  70. cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1)
  71. width += w
  72. i += len(cluster) - 1
  73. pstate = parser.GroundState
  74. continue
  75. }
  76. if action == parser.PrintAction {
  77. width++
  78. }
  79. pstate = state
  80. }
  81. return width
  82. }