gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

ansi.go (8371B)


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