This commit is contained in:
Sasha Koshka 2023-04-07 23:03:42 -04:00
parent f21a41982e
commit 43a664009c
2 changed files with 227 additions and 25 deletions

View File

@ -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

View File

@ -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
}