361 lines
10 KiB
Go
361 lines
10 KiB
Go
|
package ansi
|
||
|
|
||
|
import "strconv"
|
||
|
import "strings"
|
||
|
import "image/color"
|
||
|
|
||
|
// Useful: https://en.wikipedia.org/wiki/ANSI_escape_code
|
||
|
|
||
|
// Decoder is a state machine capable of decoding text contianing various escape
|
||
|
// codes and sequences. It satisfies io.Writer. It has no constructor and its
|
||
|
// zero value can be used safely.
|
||
|
type Decoder struct {
|
||
|
// OnText is called when a segment of text is processed.
|
||
|
OnText func (string)
|
||
|
|
||
|
// OnC0 is called when a C0 control code is processed that isn't a
|
||
|
// whitespace character.
|
||
|
OnC0 func (C0)
|
||
|
|
||
|
// OnC1 is called when a C1 escape sequence is processed.
|
||
|
OnC1 func (C1)
|
||
|
|
||
|
// OnDCS is called when a device control string is processed.
|
||
|
OnDCS func (string)
|
||
|
|
||
|
// OnCSI is called when a non-SGR CSI escape sequence with no parameters
|
||
|
// is processed.
|
||
|
OnCSI func (CSI)
|
||
|
|
||
|
// OnSGR is called when a CSI SGR escape sequence with no parameters is
|
||
|
// processed.
|
||
|
OnSGR func (SGR)
|
||
|
|
||
|
// OnPM is called when a privacy message is processed.
|
||
|
OnPM func (string)
|
||
|
|
||
|
// OnAPC is called when an application program command is processed.
|
||
|
OnAPC func (string)
|
||
|
|
||
|
// Non-SGR CSI sequences with parameters:
|
||
|
OnCursorUp func (distance int)
|
||
|
OnCursorDown func (distance int)
|
||
|
OnCursorForward func (distance int)
|
||
|
OnCursorBack func (distance int)
|
||
|
OnCursorNextLine func (distance int)
|
||
|
OnCursorPreviousLine func (distance int)
|
||
|
OnCursorHorizontalAbsolute func (column int)
|
||
|
OnCursorPosition func (column, row int)
|
||
|
OnEraseInDisplay func (mode int)
|
||
|
OnEraseInLine func (mode int)
|
||
|
OnScrollUp func (distance int)
|
||
|
OnScrollDown func (distance int)
|
||
|
OnHorizontalVerticalPosition func (column, row int)
|
||
|
|
||
|
// SGR CSI sequences with parameters:
|
||
|
OnForegroundColor func (Color)
|
||
|
OnForegroundColorTrue func (color.RGBA)
|
||
|
OnBackgroundColor func (Color)
|
||
|
OnBackgroundColorTrue func (color.RGBA)
|
||
|
OnUnderlineColor func (Color)
|
||
|
OnUnderlineColorTrue func (color.RGBA)
|
||
|
|
||
|
// OSC sequences from XTerm:
|
||
|
OnWindowTitle func (title string)
|
||
|
OnIconName func (name string)
|
||
|
OnIconFile func (path string)
|
||
|
OnXProperty func (property, value string)
|
||
|
OnSelectionPut func (selection, text string)
|
||
|
OnSelectionGet func (selection string)
|
||
|
OnQueryAllowed func ()
|
||
|
OnQueryDisallowed func ()
|
||
|
|
||
|
// OSC sequences from iTerm2:
|
||
|
OnCursorShape func (shape int)
|
||
|
OnHyperlink func (params map[string] string, text, link string)
|
||
|
OnBackgroundImage func (path string)
|
||
|
|
||
|
state decodeState
|
||
|
expectingST bool
|
||
|
gathered []byte
|
||
|
}
|
||
|
|
||
|
type decodeState int; const (
|
||
|
decodeStateText decodeState = iota
|
||
|
decodeStateAwaitC1
|
||
|
|
||
|
decodeStateGatherDCS
|
||
|
decodeStateGatherSOS
|
||
|
decodeStateGatherCSI
|
||
|
decodeStateGatherOSC
|
||
|
decodeStateGatherPM
|
||
|
decodeStateGatherAPC
|
||
|
)
|
||
|
|
||
|
func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) {
|
||
|
wrote = len(buffer)
|
||
|
|
||
|
for len(buffer) > 0 {
|
||
|
switch decoder.state {
|
||
|
case decodeStateText:
|
||
|
if C0_Escape.Is(buffer[0]) {
|
||
|
// begin C1 control code
|
||
|
decoder.state = decodeStateAwaitC1
|
||
|
buffer = buffer[1:]
|
||
|
|
||
|
} else if buffer[0] < ' ' {
|
||
|
// process C0 control code
|
||
|
if decoder.OnC0 != nil {
|
||
|
decoder.OnC0(C0(buffer[0]))
|
||
|
}
|
||
|
buffer = buffer[1:]
|
||
|
|
||
|
} else {
|
||
|
// process as much plain text as we can
|
||
|
buffer = decoder.processString(buffer)
|
||
|
}
|
||
|
|
||
|
case decodeStateAwaitC1:
|
||
|
if buffer[0] < 128 {
|
||
|
// false alarm, this is just a C0 escape
|
||
|
if decoder.OnC0 != nil {
|
||
|
decoder.OnC0(C0_Escape)
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
switch C1(buffer[0]) {
|
||
|
case C1_DeviceControlString:
|
||
|
decoder.state = decodeStateGatherDCS
|
||
|
case C1_StartOfString:
|
||
|
decoder.state = decodeStateGatherSOS
|
||
|
case C1_ControlSequenceIntroducer:
|
||
|
decoder.state = decodeStateGatherCSI
|
||
|
case C1_OperatingSystemCommand:
|
||
|
decoder.state = decodeStateGatherOSC
|
||
|
case C1_PrivacyMessage:
|
||
|
decoder.state = decodeStateGatherPM
|
||
|
case C1_ApplicationProgramCommand:
|
||
|
decoder.state = decodeStateGatherAPC
|
||
|
default:
|
||
|
// process C1 control code
|
||
|
if decoder.OnC1 != nil {
|
||
|
decoder.OnC1(C1(buffer[0]))
|
||
|
}
|
||
|
}
|
||
|
buffer = buffer[1:]
|
||
|
|
||
|
case
|
||
|
decodeStateGatherDCS,
|
||
|
decodeStateGatherSOS,
|
||
|
decodeStateGatherOSC,
|
||
|
decodeStateGatherPM,
|
||
|
decodeStateGatherAPC:
|
||
|
|
||
|
if decoder.expectingST && C1_StringTerminator.Is(buffer[0]) {
|
||
|
// remove the trailing ESC
|
||
|
decoder.gathered = decoder.gathered [
|
||
|
:len(decoder.gathered) - 1]
|
||
|
|
||
|
if decoder.state == decodeStateGatherOSC {
|
||
|
// we understand some OSC codes so we
|
||
|
// handle them differently
|
||
|
decoder.processOSC()
|
||
|
} else {
|
||
|
// generic handler for uncommon stuff
|
||
|
decoder.processGeneric()
|
||
|
}
|
||
|
decoder.state = decodeStateText
|
||
|
}
|
||
|
if C0_Escape.Is(buffer[0]) {
|
||
|
decoder.expectingST = true
|
||
|
}
|
||
|
|
||
|
case decodeStateGatherCSI:
|
||
|
decoder.gather(buffer[0])
|
||
|
if buffer[0] < 0x30 || buffer[0] > 0x3F {
|
||
|
decoder.processCSI()
|
||
|
decoder.state = decodeStateText
|
||
|
}
|
||
|
buffer = buffer[1:]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (decoder *Decoder) gather (character byte) {
|
||
|
decoder.gathered = append(decoder.gathered, character)
|
||
|
}
|
||
|
|
||
|
func (decoder *Decoder) processString (buffer []byte) []byte {
|
||
|
for index, char := range buffer {
|
||
|
if C0_Escape.Is(char) {
|
||
|
if decoder.OnText != nil {
|
||
|
decoder.OnText(string(buffer[:index]))
|
||
|
}
|
||
|
return buffer[:index]
|
||
|
}
|
||
|
}
|
||
|
return buffer
|
||
|
}
|
||
|
|
||
|
func (decoder *Decoder) processGeneric () {
|
||
|
parameter := string(decoder.gathered)
|
||
|
switch decoder.state {
|
||
|
case decodeStateGatherDCS:
|
||
|
if decoder.OnDCS != nil { decoder.OnDCS(parameter) }
|
||
|
case decodeStateGatherSOS:
|
||
|
if decoder.OnText != nil { decoder.OnText(parameter) }
|
||
|
case decodeStateGatherPM:
|
||
|
if decoder.OnPM != nil { decoder.OnPM(parameter) }
|
||
|
case decodeStateGatherAPC:
|
||
|
if decoder.OnAPC != nil { decoder.OnAPC(parameter) }
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (decoder *Decoder) processOSC () {
|
||
|
// TODO: analyze OSC
|
||
|
}
|
||
|
|
||
|
func (decoder *Decoder) processCSI () {
|
||
|
if len(decoder.gathered) < 2 { return }
|
||
|
parameters := ParameterInts(decoder.gathered)
|
||
|
|
||
|
var p0, p1, p2, p3 int
|
||
|
if len(parameters) > 0 { p0 = parameters[0] }
|
||
|
if len(parameters) > 1 { p1 = parameters[1] }
|
||
|
if len(parameters) > 2 { p2 = parameters[2] }
|
||
|
if len(parameters) > 3 { p3 = parameters[3] }
|
||
|
|
||
|
switch Last(decoder.gathered) {
|
||
|
case 'A': if decoder.OnCursorUp != nil { decoder.OnCursorUp(clampOne(p0)) }
|
||
|
case 'B': if decoder.OnCursorDown != nil { decoder.OnCursorDown(clampOne(p0)) }
|
||
|
case 'C': if decoder.OnCursorForward != nil { decoder.OnCursorForward(clampOne(p0)) }
|
||
|
case 'D': if decoder.OnCursorBack != nil { decoder.OnCursorBack(clampOne(p0)) }
|
||
|
case 'E': if decoder.OnCursorNextLine != nil { decoder.OnCursorNextLine(clampOne(p0)) }
|
||
|
case 'F': if decoder.OnCursorPreviousLine != nil { decoder.OnCursorPreviousLine(clampOne(p0)) }
|
||
|
case 'G': if decoder.OnCursorHorizontalAbsolute != nil { decoder.OnCursorHorizontalAbsolute(clampOne(p0)) }
|
||
|
case 'H', 'f': if decoder.OnCursorPosition != nil { decoder.OnCursorPosition(clampOne(p0), clampOne(p1)) }
|
||
|
|
||
|
case 'J': if decoder.OnEraseInDisplay != nil { decoder.OnEraseInDisplay(p0) }
|
||
|
case 'K': if decoder.OnEraseInLine != nil { decoder.OnEraseInLine(p0) }
|
||
|
case 'S': if decoder.OnScrollUp != nil { decoder.OnScrollUp(clampOne(p0)) }
|
||
|
case 'T': if decoder.OnScrollDown != nil { decoder.OnScrollDown(clampOne(p0)) }
|
||
|
|
||
|
case 'm':
|
||
|
p0 := SGR(p0)
|
||
|
switch {
|
||
|
case
|
||
|
p0 >= SGR_ForegroundColorBlack &&
|
||
|
p0 <= SGR_ForegroundColorWhite &&
|
||
|
decoder.OnForegroundColor != nil :
|
||
|
decoder.OnForegroundColor(Color(p0 - SGR_ForegroundColorBlack))
|
||
|
case
|
||
|
p0 >= SGR_ForegroundColorBrightBlack &&
|
||
|
p0 <= SGR_ForegroundColorBrightWhite &&
|
||
|
decoder.OnForegroundColor != nil :
|
||
|
decoder.OnForegroundColor(Color(p0 - SGR_ForegroundColorBrightBlack + 8))
|
||
|
|
||
|
case p0 == SGR_ForegroundColor:
|
||
|
switch p1 {
|
||
|
case 2:
|
||
|
if decoder.OnForegroundColor == nil { break }
|
||
|
decoder.OnForegroundColor(Color(p1))
|
||
|
case 5:
|
||
|
if decoder.OnForegroundColorTrue == nil { break }
|
||
|
decoder.OnForegroundColorTrue (color.RGBA {
|
||
|
R: uint8(p1),
|
||
|
G: uint8(p2),
|
||
|
B: uint8(p3),
|
||
|
A: 0xFF,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
case
|
||
|
p0 >= SGR_BackgroundColorBlack &&
|
||
|
p0 <= SGR_BackgroundColorWhite &&
|
||
|
decoder.OnBackgroundColor != nil :
|
||
|
decoder.OnBackgroundColor(Color(p0 - SGR_BackgroundColorBlack))
|
||
|
case
|
||
|
p0 >= SGR_BackgroundColorBrightBlack &&
|
||
|
p0 <= SGR_BackgroundColorBrightWhite &&
|
||
|
decoder.OnBackgroundColor != nil :
|
||
|
decoder.OnBackgroundColor(Color(p0 - SGR_BackgroundColorBrightBlack + 8))
|
||
|
|
||
|
case p0 == SGR_BackgroundColor:
|
||
|
switch p1 {
|
||
|
case 2:
|
||
|
if decoder.OnBackgroundColor == nil { break }
|
||
|
decoder.OnBackgroundColor(Color(p1))
|
||
|
case 5:
|
||
|
if decoder.OnBackgroundColorTrue == nil { break }
|
||
|
decoder.OnBackgroundColorTrue (color.RGBA {
|
||
|
R: uint8(p1),
|
||
|
G: uint8(p2),
|
||
|
B: uint8(p3),
|
||
|
A: 0xFF,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
case p0 == SGR_UnderlineColor:
|
||
|
switch p1 {
|
||
|
case 2:
|
||
|
if decoder.OnUnderlineColor == nil { break }
|
||
|
decoder.OnUnderlineColor(Color(p1))
|
||
|
case 5:
|
||
|
if decoder.OnUnderlineColorTrue == nil { break }
|
||
|
decoder.OnUnderlineColorTrue (color.RGBA {
|
||
|
R: uint8(p1),
|
||
|
G: uint8(p2),
|
||
|
B: uint8(p3),
|
||
|
A: 0xFF,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
default: if decoder.OnSGR != nil { decoder.OnSGR(SGR(p0)) }
|
||
|
}
|
||
|
|
||
|
// TODO
|
||
|
case 'n':
|
||
|
case 's':
|
||
|
case 'u':
|
||
|
case 'h':
|
||
|
case 'l':
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func clampOne (number int) int {
|
||
|
if number < 1 {
|
||
|
return 1
|
||
|
} else {
|
||
|
return number
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Last returns the last item of a slice.
|
||
|
func Last[T any] (source []T) T {
|
||
|
return source[len(source) - 1]
|
||
|
}
|
||
|
|
||
|
// ParameterStrings separates a byte slice by semicolons into a list of strings.
|
||
|
func ParameterStrings (source []byte) (parameters []string) {
|
||
|
parameters = strings.Split(string(source), ";")
|
||
|
for index := range parameters {
|
||
|
parameters[index] = strings.TrimSpace(parameters[index])
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// ParameterInts is like ParameterStrings, but returns integers instead of
|
||
|
// strings. If a parameter is empty or cannot be converted into an integer, that
|
||
|
// parameter will be zero.
|
||
|
func ParameterInts (source []byte) (parameters []int) {
|
||
|
stringParameters := ParameterStrings(source)
|
||
|
parameters = make([]int, len(stringParameters))
|
||
|
for index, parameter := range stringParameters {
|
||
|
parameters[index], _ = strconv.Atoi(parameter)
|
||
|
}
|
||
|
return
|
||
|
}
|