ansi.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package ansi
  2. import (
  3. "regexp"
  4. "strings"
  5. "unicode"
  6. )
  7. func expand(text string) [][]string {
  8. r := regexp.MustCompile(`(?s)((?:\x1b\[.*?m)*)(.)(?:\x1b\[0m)?`)
  9. return r.FindAllStringSubmatch(text, -1)
  10. }
  11. func Apply(text string, style string) string {
  12. expanded := expand(text)
  13. result := ""
  14. for _, match := range expanded {
  15. prefix := match[1]
  16. letter := match[2]
  17. if letter == "\n" {
  18. result += "\n"
  19. continue
  20. }
  21. result += "\x1b[" + style + "m" + prefix + letter + "\x1b[0m"
  22. }
  23. return result
  24. }
  25. func Indent(text string, prefix string, includeFirst bool) string {
  26. expanded := expand(text)
  27. result := ""
  28. if includeFirst {
  29. result = prefix
  30. }
  31. for _, match := range expanded {
  32. full := match[0]
  33. letter := match[2]
  34. if letter == "\n" {
  35. result += "\n" + prefix
  36. continue
  37. }
  38. result += full
  39. }
  40. return result
  41. }
  42. const suffix = " "
  43. func Pad(text string, length int) string {
  44. expanded := expand(text)
  45. result := ""
  46. lineLength := 0
  47. for _, match := range expanded {
  48. full := match[0]
  49. letter := match[2]
  50. if letter == "\n" {
  51. amount := length - lineLength
  52. if amount <= 0 {
  53. result += "\n"
  54. lineLength = 0
  55. continue
  56. }
  57. result += strings.Repeat(suffix, amount) + "\n"
  58. lineLength = 0
  59. continue
  60. }
  61. lineLength += 1
  62. result += full
  63. }
  64. /* Final line */
  65. amount := length - lineLength
  66. if amount > 0 {
  67. result += strings.Repeat(suffix, amount)
  68. }
  69. return result
  70. }
  71. /*
  72. I am not convinced this works perfectly, but it is well-tested,
  73. so I will call it good for now.
  74. */
  75. func Wrap(text string, length int) string {
  76. expanded := expand(text)
  77. result := []string{}
  78. var line, space, word string
  79. var lineLength, spaceLength, wordLength int
  80. for _, match := range expanded {
  81. full := match[0]
  82. letter := match[2]
  83. /* TODO: I need to find the list of non-breaking whitespace characters
  84. to exclude from this conditional */
  85. if !unicode.IsSpace([]rune(letter)[0]) {
  86. if wordLength == length {
  87. /*
  88. Word fills an entire line; push it as a line
  89. (we know this won't clobber stuff in `line`, because the word has
  90. already necessarily forced line to be pushed)
  91. */
  92. result = append(result, word)
  93. line = ""; lineLength = 0
  94. space = ""; spaceLength = 0
  95. word = ""; wordLength = 0
  96. }
  97. if lineLength + spaceLength + wordLength >= length {
  98. /* The word no longer fits on the current line; push the current line */
  99. result = append(result, line)
  100. line = ""; lineLength = 0
  101. space = ""; spaceLength = 0
  102. }
  103. word += full; wordLength += 1
  104. continue
  105. }
  106. /* This means whitespace has been encountered; if there's a word, add it to the line */
  107. if wordLength > 0 {
  108. line += space + word; lineLength += spaceLength + wordLength
  109. space = ""; spaceLength = 0
  110. word = ""; wordLength = 0
  111. }
  112. if letter == "\n" {
  113. /*
  114. If the spaces can be jammed into the line, add them.
  115. This ensures that Wrap(Pad(*)) doesn't eliminate the
  116. padding.
  117. */
  118. if lineLength + spaceLength <= length {
  119. line += space; lineLength += spaceLength
  120. }
  121. /* Add the current line as-is and clear everything */
  122. result = append(result, line)
  123. line = ""; lineLength = 0
  124. space = ""; spaceLength = 0
  125. word = ""; wordLength = 0
  126. } else {
  127. space += full; spaceLength += 1
  128. }
  129. }
  130. /* Cleanup */
  131. if wordLength > 0 {
  132. line += space + word; lineLength += spaceLength + wordLength
  133. }
  134. finalLetter := ""
  135. if len(expanded) > 0 {
  136. finalLetter = expanded[len(expanded)-1][2]
  137. }
  138. if lineLength > 0 || finalLetter == "\n" {
  139. result = append(result, line)
  140. }
  141. return strings.Join(result, "\n")
  142. }
  143. func DumbWrap(text string, width int) string {
  144. expanded := expand(text)
  145. result := ""
  146. currentLineLength := 0
  147. for _, match := range expanded {
  148. full := match[0]
  149. letter := match[2]
  150. if letter == "\n" {
  151. currentLineLength = 0
  152. result += "\n"
  153. continue
  154. }
  155. if currentLineLength == width {
  156. currentLineLength = 0
  157. result += "\n"
  158. }
  159. result += full
  160. currentLineLength += 1
  161. }
  162. return result
  163. }
  164. /*
  165. TODO:
  166. add `Scrub` function that removes all ANSI codes from text
  167. (this will be used when people redirect output to file)
  168. add `Snip` function that limits text to a certain number
  169. of lines, adding an ellipsis if this required removing
  170. some text
  171. add `Squash` function that converts newlines to spaces
  172. (this will be used to prevent newlines from appearing
  173. in things like names and titles)
  174. add `StrictWrap` function that wraps not based on whitespace
  175. but strictly on length (this will be used for code blocks)
  176. */