ansi.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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 collapse(expanded [][]string) string {
  12. output := ""
  13. for _, match := range expanded {
  14. output += match[0]
  15. }
  16. return output
  17. }
  18. func Apply(text string, style string) string {
  19. expanded := expand(text)
  20. result := ""
  21. for _, match := range expanded {
  22. prefix := match[1]
  23. letter := match[2]
  24. if letter == "\n" {
  25. result += "\n"
  26. continue
  27. }
  28. result += "\x1b[" + style + "m" + prefix + letter + "\x1b[0m"
  29. }
  30. return result
  31. }
  32. func Indent(text string, prefix string, includeFirst bool) string {
  33. expanded := expand(text)
  34. result := ""
  35. if includeFirst {
  36. result = prefix
  37. }
  38. for _, match := range expanded {
  39. full := match[0]
  40. letter := match[2]
  41. if letter == "\n" {
  42. result += "\n" + prefix
  43. continue
  44. }
  45. result += full
  46. }
  47. return result
  48. }
  49. const suffix = " "
  50. func Pad(text string, length int) string {
  51. expanded := expand(text)
  52. result := ""
  53. lineLength := 0
  54. for _, match := range expanded {
  55. full := match[0]
  56. letter := match[2]
  57. if letter == "\n" {
  58. amount := length - lineLength
  59. if amount <= 0 {
  60. result += "\n"
  61. lineLength = 0
  62. continue
  63. }
  64. result += strings.Repeat(suffix, amount) + "\n"
  65. lineLength = 0
  66. continue
  67. }
  68. lineLength += 1
  69. result += full
  70. }
  71. /* Final line */
  72. amount := length - lineLength
  73. if amount > 0 {
  74. result += strings.Repeat(suffix, amount)
  75. }
  76. return result
  77. }
  78. /*
  79. I am not convinced this works perfectly, but it is well-tested,
  80. so I will call it good for now.
  81. */
  82. func Wrap(text string, length int) string {
  83. expanded := expand(text)
  84. result := []string{}
  85. var line, space, word string
  86. var lineLength, spaceLength, wordLength int
  87. for _, match := range expanded {
  88. full := match[0]
  89. letter := match[2]
  90. if !unicode.IsSpace([]rune(letter)[0]) {
  91. if wordLength == length {
  92. /*
  93. Word fills an entire line; push it as a line
  94. (we know this won't clobber stuff in `line`, because the word has
  95. already necessarily forced line to be pushed)
  96. */
  97. result = append(result, word)
  98. line = ""
  99. lineLength = 0
  100. space = ""
  101. spaceLength = 0
  102. word = ""
  103. wordLength = 0
  104. }
  105. if lineLength+spaceLength+wordLength >= length {
  106. /* The word no longer fits on the current line; push the current line */
  107. result = append(result, line)
  108. line = ""
  109. lineLength = 0
  110. space = ""
  111. spaceLength = 0
  112. }
  113. word += full
  114. wordLength += 1
  115. continue
  116. }
  117. /* This means whitespace has been encountered; if there's a word, add it to the line */
  118. if wordLength > 0 {
  119. line += space + word
  120. lineLength += spaceLength + wordLength
  121. space = ""
  122. spaceLength = 0
  123. word = ""
  124. wordLength = 0
  125. }
  126. if letter == "\n" {
  127. /*
  128. If the spaces can be jammed into the line, add them.
  129. This ensures that Wrap(Pad(*)) doesn't eliminate the
  130. padding.
  131. */
  132. if lineLength+spaceLength <= length {
  133. line += space
  134. lineLength += spaceLength
  135. }
  136. /* Add the current line as-is and clear everything */
  137. result = append(result, line)
  138. line = ""
  139. lineLength = 0
  140. space = ""
  141. spaceLength = 0
  142. word = ""
  143. wordLength = 0
  144. } else {
  145. space += full
  146. spaceLength += 1
  147. }
  148. }
  149. /* Cleanup */
  150. if wordLength > 0 {
  151. line += space + word
  152. lineLength += spaceLength + wordLength
  153. }
  154. finalLetter := ""
  155. if len(expanded) > 0 {
  156. finalLetter = expanded[len(expanded)-1][2]
  157. }
  158. if lineLength > 0 || finalLetter == "\n" {
  159. result = append(result, line)
  160. }
  161. return strings.Join(result, "\n")
  162. }
  163. func DumbWrap(text string, width int) string {
  164. expanded := expand(text)
  165. result := ""
  166. currentLineLength := 0
  167. for _, match := range expanded {
  168. full := match[0]
  169. letter := match[2]
  170. if letter == "\n" {
  171. currentLineLength = 0
  172. result += "\n"
  173. continue
  174. }
  175. if currentLineLength == width {
  176. currentLineLength = 0
  177. result += "\n"
  178. }
  179. result += full
  180. currentLineLength += 1
  181. }
  182. return result
  183. }
  184. /*
  185. Limits `text` to the given `height` and `width`, adding an
  186. ellipsis to the end and omitting trailing whitespace-only lines
  187. */
  188. func Snip(text string, width, height int, ellipsis string) string {
  189. snipped := make([]string, 0, height)
  190. /* This split is fine because newlines are
  191. guaranteed to not be wrapped in ansi codes */
  192. lines := strings.Split(text, "\n")
  193. requiresEllipsis := false
  194. if len(lines) <= height {
  195. height = len(lines)
  196. } else {
  197. requiresEllipsis = true
  198. }
  199. /* Adding from back to front */
  200. for i := height - 1; i >= 0; i -= 1 {
  201. line := expand(lines[i])
  202. if len(snipped) == 0 {
  203. if lineIsOnlyWhitespace(line) {
  204. requiresEllipsis = true
  205. continue
  206. }
  207. /* Remove last character to make way for ellipsis */
  208. if len(line) == width && requiresEllipsis {
  209. line = line[:len(line)-1]
  210. }
  211. }
  212. snipped = append([]string{collapse(line)}, snipped...)
  213. }
  214. output := strings.Join(snipped, "\n")
  215. if requiresEllipsis {
  216. output += ellipsis
  217. }
  218. return output
  219. }
  220. func lineIsOnlyWhitespace(expanded [][]string) bool {
  221. for _, match := range expanded {
  222. if !unicode.IsSpace([]rune(match[2])[0]) {
  223. return false
  224. }
  225. }
  226. return true
  227. }
  228. func Height(text string) uint {
  229. return uint(strings.Count(text, "\n")) + 1
  230. }
  231. func CenterVertically(prefix, centered, suffix string, height uint) string {
  232. prefixHeight, centeredHeight, suffixHeight := Height(prefix), Height(centered), Height(suffix)
  233. if height <= centeredHeight {
  234. return strings.Join(strings.Split(centered, "\n")[:height], "\n")
  235. }
  236. totalBufferSize := height - centeredHeight
  237. topBufferSize := totalBufferSize / 2
  238. bottomBufferSize := topBufferSize + totalBufferSize%2
  239. if topBufferSize > prefixHeight {
  240. prefix = strings.Repeat("\n", int(topBufferSize-prefixHeight)) + prefix
  241. } else if topBufferSize < prefixHeight {
  242. prefix = strings.Join(strings.Split(prefix, "\n")[prefixHeight-topBufferSize:], "\n")
  243. }
  244. if bottomBufferSize > suffixHeight {
  245. suffix += strings.Repeat("\n", int(bottomBufferSize-suffixHeight))
  246. } else if bottomBufferSize < suffixHeight {
  247. suffix = strings.Join(strings.Split(suffix, "\n")[:bottomBufferSize], "\n")
  248. }
  249. return prefix + "\n" + centered + "\n" + suffix
  250. }
  251. func ReplaceLastLine(original, replacement string) string {
  252. if strings.Contains(replacement, "\n") {
  253. panic("new version of last line cannot contain a newline")
  254. }
  255. var lastIndex = strings.LastIndex(original, "\n")
  256. if lastIndex == -1 {
  257. lastIndex = 0
  258. }
  259. return original[:lastIndex] + "\n" + replacement
  260. }
  261. func SetLength(text string, length int, ellipsis string) string {
  262. text = Squash(Scrub(text))
  263. runes := []rune(text)
  264. if length == 0 {
  265. return ""
  266. }
  267. if len(runes) > length {
  268. return string(runes[:length-1]) + ellipsis
  269. }
  270. if len(runes) < length {
  271. return string(runes) + strings.Repeat(" ", length-len(runes))
  272. }
  273. return text
  274. }
  275. func Squash(text string) string {
  276. return strings.ReplaceAll(text, "\n", " ")
  277. }
  278. func Scrub(text string) string {
  279. text = strings.ReplaceAll(text, "\t", " ")
  280. text = strings.Map(func(input rune) rune {
  281. if input != '\n' && unicode.IsControl(input) {
  282. return -1
  283. }
  284. return input
  285. }, text)
  286. return text
  287. }