package door import ( "bytes" "fmt" "log" "regexp" "strconv" "strings" ) var FindANSIColor *regexp.Regexp const WRITE_DEBUG bool = false func init() { FindANSIColor = regexp.MustCompile("\x1b\\[([0-9;]*)m") } /* find \x1b[0;1;37;40m codes. return array of start/end positions in the given string. */ func find_ansicolor(text string) [][]int { var color_codes [][]int // word, _ := regexp.Compile("\x1b\\[([0-9;]+)m") var colors [][]int = FindANSIColor.FindAllStringIndex(text, -1) // regexp seems to be ignoring the capture groups. // colors := word.FindAllSubmatchIndex([]byte(text), len(text)) for _, pos := range colors { var txt string = text[pos[0]+2 : pos[1]-1] if txt == "" { txt = "0" } // log.Printf("Text: [%s]\n", txt) var codes []string = strings.Split(txt, ";") // log.Printf("Codes: [%#v]\n", codes) var code []int = make([]int, len(codes)) for idx, c := range codes { var err error code[idx], err = strconv.Atoi(c) if err != nil { log.Printf("Atoi: %#v [%s]\n", err, c) } color_codes = append(color_codes, code) } } return color_codes } type ANSIColorParts struct { Bold bool Blink bool FG int BG int } // Parse an array of codes into their ANSIColorParts. func ParseColorArray(colorarray []int) ANSIColorParts { var acp ANSIColorParts acp.FG = -1 acp.BG = -1 if len(colorarray) == 0 { colorarray = []int{0} } for _, c := range colorarray { switch c { case 0: acp.FG = 7 acp.BG = 0 acp.Bold = false acp.Blink = false case 1: acp.Bold = true case 5: acp.Blink = true case 30, 31, 32, 33, 34, 35, 36, 37: acp.FG = c - 30 case 40, 41, 42, 43, 44, 45, 46, 47: acp.BG = c - 40 } } return acp } // Update the LastColor with the latest codes. func (d *Door) UpdateLastColor(output string, LastColor *[]int) { // use the last color information + whatever is in the string to // track the last color set var updated ANSIColorParts = ParseColorArray(*LastColor) var colors [][]int = find_ansicolor(output) for _, codes := range colors { if codes[0] == 0 { updated = ParseColorArray(codes) } else { newCode := ParseColorArray(codes) if newCode.Bold { updated.Bold = true } if newCode.Blink { updated.Blink = true } if (newCode.FG != -1) && (newCode.FG != updated.FG) { updated.FG = newCode.FG } if (newCode.BG != -1) && (newCode.BG != updated.BG) { updated.BG = newCode.BG } } } *LastColor = make([]int, 1) (*LastColor)[0] = 0 if updated.Blink { *LastColor = append(*LastColor, 5) } if updated.Bold { *LastColor = append(*LastColor, 1) } if updated.FG != -1 { *LastColor = append(*LastColor, updated.FG+30) } if updated.BG != -1 { *LastColor = append(*LastColor, updated.BG+40) } } /* reading from a closed channel is easy to detect. res, ok := <-channel ok == false writing to a closed channel is a panic. Have the Write manage itself. */ // For now, update is SavePos + output + RestorePos. // FUTURE: Access the screen struct to "save" Color+Attr+Pos. func (d *Door) Update(output []byte) { // sync.Pool ? d.WriteA(SAVE_POS, output, RESTORE_POS) /* var buffer bytes.Buffer buffer.WriteString(SAVE_POS) buffer.Write(output) buffer.WriteString(RESTORE_POS) d.Writer.Write(buffer.Bytes()) */ // d.Write(SAVE_POS + output + RESTORE_POS) } func (d *Door) WriteS(output string) { d.Write([]byte(output)) } func (d *Door) WriteA(a ...interface{}) { d.writeMutex.Lock() defer d.writeMutex.Unlock() for _, item := range a { switch item.(type) { case string: d.LockedWrite([]byte(item.(string))) case []byte: d.LockedWrite(item.([]byte)) case *bytes.Buffer: d.LockedWrite(item.(*bytes.Buffer).Bytes()) default: log.Printf("Unknown/unsupported type: %T\n", item) } } } // Is this byte the end of the ANSI CSI? func EndCSI(c byte) bool { return (c >= 0x40) && (c <= 0x7f) } func (d *Door) ANSIProcess() { var csi byte = d.ansiCode[len(d.ansiCode)-1] switch csi { case 's': // Save Color var slen int = len(d.LastColor) if cap(d.Writer.LastSavedColor) < slen { d.Writer.LastSavedColor = make([]int, slen) } else { d.Writer.LastSavedColor = d.Writer.LastSavedColor[0:slen] } copy(d.Writer.LastSavedColor, d.LastColor) if WRITE_DEBUG { log.Printf("ColorSave: %d\n", d.Writer.LastSavedColor) } case 'u': // Restore Color var slen int = len(d.Writer.LastSavedColor) if cap(d.LastColor) < slen { d.LastColor = make([]int, slen) } else { d.LastColor = d.LastColor[0:slen] } copy(d.LastColor, d.Writer.LastSavedColor) if WRITE_DEBUG { log.Printf("ColorRestore: %d\n", d.LastColor) } case 'm': // Process color code var color [5]int var cpos int var code int var pos int for pos = 0; pos < len(d.ansiCode); pos++ { var b byte = d.ansiCode[pos] switch b { case ';': color[cpos] = code code = 0 cpos++ case '1', '2', '3', '4', '5', '6', '7', '8', '9', '0': code *= 10 code += int(b - '0') } } if code != 0 { color[cpos] = code code = 0 cpos++ } // Make sure there is always at least one code here. if cpos == 0 { color[cpos] = 0 cpos++ } if WRITE_DEBUG { log.Printf("color: [%d]", color[0:cpos]) } var newColor = ParseColorArray(d.LastColor) for _, c := range color[0:cpos] { switch c { case 0: newColor.FG = 7 newColor.BG = 0 newColor.Bold = false newColor.Blink = false case 1: newColor.Bold = true case 5: newColor.Blink = true case 30, 31, 32, 33, 34, 35, 36, 37: newColor.FG = c - 30 case 40, 41, 42, 43, 44, 45, 46, 47: newColor.BG = c - 40 } } // Reset LastColor d.LastColor = d.LastColor[0:0] d.LastColor = append(d.LastColor, 0) if newColor.Blink { d.LastColor = append(d.LastColor, 5) } if newColor.Bold { d.LastColor = append(d.LastColor, 1) } if newColor.FG != -1 { d.LastColor = append(d.LastColor, newColor.FG+30) } if newColor.BG != -1 { d.LastColor = append(d.LastColor, newColor.BG+40) } if WRITE_DEBUG { log.Printf("LastColor: [%d]", d.LastColor) } } if WRITE_DEBUG { var outputByte [20]byte var output []byte = outputByte[0:0] output = fmt.Appendf(output, "ANSI: [%q]\n", d.ansiCode) log.Printf("%s", output) } // Reset d.ansiCode = d.ansiCode[0:0] d.ansiCSI = false } func (d *Door) ANSIScan(output []byte) { var pos, nextpos int var olen int = len(output) var c byte if d.ansiCSI { for pos < olen { c = output[pos] d.ansiCode = append(d.ansiCode, c) pos++ if EndCSI(c) { d.ANSIProcess() break } } if pos == olen { return // pos == -1 // end } } for pos != -1 { nextpos = bytes.Index(output[pos:], []byte{'\x1b'}) if nextpos != -1 { // Found ESC nextpos += pos + 1 if output[nextpos] == '[' { d.ansiCSI = true nextpos++ for nextpos < olen { c = output[nextpos] d.ansiCode = append(d.ansiCode, c) nextpos++ if EndCSI(c) { d.ANSIProcess() break } } } pos = nextpos } else { return } } } func (d *Door) NewLinesTranslate(output []byte) []byte { var pos, nextpos int if d.nlBuffer == nil { d.nlBuffer = &bytes.Buffer{} } d.nlBuffer.Reset() for pos != -1 { nextpos = bytes.Index(output[pos:], []byte{'\n'}) if nextpos != -1 { nextpos += pos // Something to do d.nlBuffer.Write(output[pos:nextpos]) nextpos++ pos = nextpos d.nlBuffer.Write([]byte("\r\n")) } else { d.nlBuffer.Write(output[pos:]) pos = nextpos // -1 } } // log.Printf(">> %q\n", ow.nlBuffer.Bytes()) return d.nlBuffer.Bytes() } func (d *Door) LockedWrite(output []byte) { if d.writeMutex.TryLock() { log.Panic("LockedWrite: mutex was NOT locked.") } /* if bytes.HasSuffix(output, []byte(RestorePos)) { // Write the current color lastColor = Color(d.LastColor) //fmt.Append(lastColor, []byte(Color(d.LastColor))) log.Printf("Restore LastColor: %d => %q", d.LastColor, lastColor) } */ if true { output = d.NewLinesTranslate(output) } if WRITE_DEBUG { log.Printf(">> %q\n", output) } d.Writer.Write(output) d.ANSIScan(output) if bytes.HasSuffix(output, []byte(RestorePos)) { lastColor := Color(d.LastColor) //fmt.Append(lastColor, []byte(Color(d.LastColor))) if WRITE_DEBUG { log.Printf("Restore LastColor: %d => %q", d.LastColor, lastColor) } d.Writer.Write([]byte(lastColor)) //d.ANSIScan([]byte(lastColor)) } /* if len(lastColor) != 0 { log.Println("Restored HERE...") d.Writer.Write([]byte(lastColor)) d.ANSIScan([]byte(lastColor)) } */ // else { // Only update if we're NOT restoring... // Update // d.ANSIScan(output) // } } func (d *Door) Write(output []byte) { if len(output) == 0 { return } d.writeMutex.Lock() defer d.writeMutex.Unlock() d.LockedWrite(output) }