ansi.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. package tview
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "strconv"
  7. "strings"
  8. )
  9. // The states of the ANSI escape code parser.
  10. const (
  11. ansiText = iota
  12. ansiEscape
  13. ansiSubstring
  14. ansiControlSequence
  15. )
  16. // ansi is a io.Writer which translates ANSI escape codes into tview color
  17. // tags.
  18. type ansi struct {
  19. io.Writer
  20. // Reusable buffers.
  21. buffer *bytes.Buffer // The entire output text of one Write().
  22. csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
  23. attributes string // The buffer's current text attributes (a tview attribute string).
  24. // The current state of the parser. One of the ansi constants.
  25. state int
  26. }
  27. // ANSIWriter returns an io.Writer which translates any ANSI escape codes
  28. // written to it into tview color tags. Other escape codes don't have an effect
  29. // and are simply removed. The translated text is written to the provided
  30. // writer.
  31. func ANSIWriter(writer io.Writer) io.Writer {
  32. return &ansi{
  33. Writer: writer,
  34. buffer: new(bytes.Buffer),
  35. csiParameter: new(bytes.Buffer),
  36. csiIntermediate: new(bytes.Buffer),
  37. state: ansiText,
  38. }
  39. }
  40. // Write parses the given text as a string of runes, translates ANSI escape
  41. // codes to color tags and writes them to the output writer.
  42. func (a *ansi) Write(text []byte) (int, error) {
  43. defer func() {
  44. a.buffer.Reset()
  45. }()
  46. for _, r := range string(text) {
  47. switch a.state {
  48. // We just entered an escape sequence.
  49. case ansiEscape:
  50. switch r {
  51. case '[': // Control Sequence Introducer.
  52. a.csiParameter.Reset()
  53. a.csiIntermediate.Reset()
  54. a.state = ansiControlSequence
  55. case 'c': // Reset.
  56. fmt.Fprint(a.buffer, "[-:-:-]")
  57. a.state = ansiText
  58. case 'P', ']', 'X', '^', '_': // Substrings and commands.
  59. a.state = ansiSubstring
  60. default: // Ignore.
  61. a.state = ansiText
  62. }
  63. // CSI Sequences.
  64. case ansiControlSequence:
  65. switch {
  66. case r >= 0x30 && r <= 0x3f: // Parameter bytes.
  67. if _, err := a.csiParameter.WriteRune(r); err != nil {
  68. return 0, err
  69. }
  70. case r >= 0x20 && r <= 0x2f: // Intermediate bytes.
  71. if _, err := a.csiIntermediate.WriteRune(r); err != nil {
  72. return 0, err
  73. }
  74. case r >= 0x40 && r <= 0x7e: // Final byte.
  75. switch r {
  76. case 'E': // Next line.
  77. count, _ := strconv.Atoi(a.csiParameter.String())
  78. if count == 0 {
  79. count = 1
  80. }
  81. fmt.Fprint(a.buffer, strings.Repeat("\n", count))
  82. case 'm': // Select Graphic Rendition.
  83. var background, foreground string
  84. params := a.csiParameter.String()
  85. fields := strings.Split(params, ";")
  86. if len(params) == 0 || len(fields) == 1 && fields[0] == "0" {
  87. // Reset.
  88. a.attributes = ""
  89. if _, err := a.buffer.WriteString("[-:-:-]"); err != nil {
  90. return 0, err
  91. }
  92. break
  93. }
  94. lookupColor := func(colorNumber int) string {
  95. if colorNumber < 0 || colorNumber > 15 {
  96. return "black"
  97. }
  98. return []string{
  99. "black",
  100. "maroon",
  101. "green",
  102. "olive",
  103. "navy",
  104. "purple",
  105. "teal",
  106. "silver",
  107. "gray",
  108. "red",
  109. "lime",
  110. "yellow",
  111. "blue",
  112. "fuchsia",
  113. "aqua",
  114. "white",
  115. }[colorNumber]
  116. }
  117. FieldLoop:
  118. for index, field := range fields {
  119. switch field {
  120. case "1", "01":
  121. if strings.IndexRune(a.attributes, 'b') < 0 {
  122. a.attributes += "b"
  123. }
  124. case "2", "02":
  125. if strings.IndexRune(a.attributes, 'd') < 0 {
  126. a.attributes += "d"
  127. }
  128. case "4", "04":
  129. if strings.IndexRune(a.attributes, 'u') < 0 {
  130. a.attributes += "u"
  131. }
  132. case "5", "05":
  133. if strings.IndexRune(a.attributes, 'l') < 0 {
  134. a.attributes += "l"
  135. }
  136. case "22":
  137. if i := strings.IndexRune(a.attributes, 'b'); i >= 0 {
  138. a.attributes = a.attributes[:i] + a.attributes[i+1:]
  139. }
  140. if i := strings.IndexRune(a.attributes, 'd'); i >= 0 {
  141. a.attributes = a.attributes[:i] + a.attributes[i+1:]
  142. }
  143. case "24":
  144. if i := strings.IndexRune(a.attributes, 'u'); i >= 0 {
  145. a.attributes = a.attributes[:i] + a.attributes[i+1:]
  146. }
  147. case "25":
  148. if i := strings.IndexRune(a.attributes, 'l'); i >= 0 {
  149. a.attributes = a.attributes[:i] + a.attributes[i+1:]
  150. }
  151. case "30", "31", "32", "33", "34", "35", "36", "37":
  152. colorNumber, _ := strconv.Atoi(field)
  153. foreground = lookupColor(colorNumber - 30)
  154. case "39":
  155. foreground = "-"
  156. case "40", "41", "42", "43", "44", "45", "46", "47":
  157. colorNumber, _ := strconv.Atoi(field)
  158. background = lookupColor(colorNumber - 40)
  159. case "49":
  160. background = "-"
  161. case "90", "91", "92", "93", "94", "95", "96", "97":
  162. colorNumber, _ := strconv.Atoi(field)
  163. foreground = lookupColor(colorNumber - 82)
  164. case "100", "101", "102", "103", "104", "105", "106", "107":
  165. colorNumber, _ := strconv.Atoi(field)
  166. background = lookupColor(colorNumber - 92)
  167. case "38", "48":
  168. var color string
  169. if len(fields) > index+1 {
  170. if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors.
  171. colorNumber, _ := strconv.Atoi(fields[index+2])
  172. if colorNumber <= 15 {
  173. color = lookupColor(colorNumber)
  174. } else if colorNumber <= 231 {
  175. red := (colorNumber - 16) / 36
  176. green := ((colorNumber - 16) / 6) % 6
  177. blue := (colorNumber - 16) % 6
  178. color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
  179. } else if colorNumber <= 255 {
  180. grey := 255 * (colorNumber - 232) / 23
  181. color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey)
  182. }
  183. } else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
  184. red, _ := strconv.Atoi(fields[index+2])
  185. green, _ := strconv.Atoi(fields[index+3])
  186. blue, _ := strconv.Atoi(fields[index+4])
  187. color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
  188. }
  189. }
  190. if len(color) > 0 {
  191. if field == "38" {
  192. foreground = color
  193. } else {
  194. background = color
  195. }
  196. }
  197. break FieldLoop
  198. }
  199. }
  200. var colon string
  201. if len(a.attributes) > 0 {
  202. colon = ":"
  203. }
  204. if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 {
  205. fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes)
  206. }
  207. }
  208. a.state = ansiText
  209. default: // Undefined byte.
  210. a.state = ansiText // Abort CSI.
  211. }
  212. // We just entered a substring/command sequence.
  213. case ansiSubstring:
  214. if r == 27 { // Most likely the end of the substring.
  215. a.state = ansiEscape
  216. } // Ignore all other characters.
  217. // "ansiText" and all others.
  218. default:
  219. if r == 27 {
  220. // This is the start of an escape sequence.
  221. a.state = ansiEscape
  222. } else {
  223. // Just a regular rune. Send to buffer.
  224. if _, err := a.buffer.WriteRune(r); err != nil {
  225. return 0, err
  226. }
  227. }
  228. }
  229. }
  230. // Write buffer to target writer.
  231. n, err := a.buffer.WriteTo(a.Writer)
  232. if err != nil {
  233. return int(n), err
  234. }
  235. return len(text), nil
  236. }
  237. // TranslateANSI replaces ANSI escape sequences found in the provided string
  238. // with tview's color tags and returns the resulting string.
  239. func TranslateANSI(text string) string {
  240. var buffer bytes.Buffer
  241. writer := ANSIWriter(&buffer)
  242. writer.Write([]byte(text))
  243. return buffer.String()
  244. }