github.com/grahambrereton-form3/tilt@v0.10.18/internal/rty/ansi.go (about)

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