diff --git a/ansi/csi.go b/ansi/csi.go index 417a2db..7fb8969 100644 --- a/ansi/csi.go +++ b/ansi/csi.go @@ -1,10 +1,9 @@ package ansi // CSI represents a list of CSI sequences that have no parameters. +// FIXME: some of these do indeed have parameters type CSI int; const ( - CSI_AuxPortOn CSI = iota - CSI_AuxPortOff - CSI_DeviceStatusReport + CSI_DeviceStatusReport CSI = iota CSI_SaveCursorPosition CSI_RestoreCursorPosition CSI_ShowCursor diff --git a/ansi/decoder.go b/ansi/decoder.go index 2dd8dce..4057e67 100644 --- a/ansi/decoder.go +++ b/ansi/decoder.go @@ -1,5 +1,7 @@ package ansi +import "strconv" +import "strings" import "image/color" // Useful: https://en.wikipedia.org/wiki/ANSI_escape_code @@ -18,6 +20,9 @@ type Decoder struct { // 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) @@ -26,6 +31,12 @@ type Decoder struct { // 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) @@ -65,14 +76,20 @@ type Decoder struct { OnBackgroundImage func (path string) state decodeState - csiParameter []byte - csiIdentifier byte + 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) { @@ -81,7 +98,7 @@ func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) { for len(buffer) > 0 { switch decoder.state { case decodeStateText: - if buffer[0] == byte(C0_Escape) { + if C0_Escape.Is(buffer[0]) { // begin C1 control code decoder.state = decodeStateAwaitC1 buffer = buffer[1:] @@ -99,24 +116,28 @@ func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) { } case decodeStateAwaitC1: - // TODO: handle OSC sequences - // TODO: handle device control string - // TODO: handle privacy message - // TODO: handle application program command - if buffer[0] < 128 { // false alarm, this is just a C0 escape if decoder.OnC0 != nil { decoder.OnC0(C0_Escape) } - - } else if buffer[0] == byte(C1_ControlSequenceIntroducer) { - // abandon all hope ye who enter here + break + } + + switch C1(buffer[0]) { + case C1_DeviceControlString: + decoder.state = decodeStateGatherDCS + case C1_StartOfString: + decoder.state = decodeStateGatherSOS + case C1_ControlSequenceIntroducer: decoder.state = decodeStateGatherCSI - decoder.csiParameter = nil - decoder.csiIdentifier = 0 - - } else { + 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])) @@ -124,14 +145,37 @@ func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) { } 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.csiIdentifier = buffer[0] decoder.processCSI() - } else { - decoder.csiParameter = append ( - decoder.csiParameter, - buffer[0]) + decoder.state = decodeStateText } buffer = buffer[1:] } @@ -140,6 +184,10 @@ func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) { 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) { @@ -152,6 +200,161 @@ func (decoder *Decoder) processString (buffer []byte) []byte { return buffer } -func (decoder *Decoder) processCSI () { - // TODO: analyze CSI parameter and id +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 }