ANSI escape code decoder wip

This commit is contained in:
Sasha Koshka 2023-04-06 13:38:47 -04:00
parent 34b79ee30d
commit f21a41982e
7 changed files with 490 additions and 9 deletions

38
ansi/c0.go Normal file
View File

@ -0,0 +1,38 @@
package ansi
// C0 represents a list of C0 control codes.
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
type C0 byte; const (
C0_Null C0 = iota
C0_StartOfHeading
C0_StartOfText
C0_EndOfText
C0_EndOfTransmission
C0_Enquiry
C0_Acknowledge
C0_Bell
C0_Backspace
C0_CharacterTab
C0_LineFeed
C0_LineTab
C0_FormFeed
C0_CarriageReturn
C0_ShiftOut
C0_ShiftIn
C0_DataLinkEscape
C0_DeviceControlOne
C0_DeviceControlTwo
C0_DeviceControlThree
C0_DeviceControlFour
C0_NegativeAcknowledge
C0_SynchronousIdle
C0_EndOfTransmissionBlock
C0_Cancel
C0_EndOfMedium
C0_Substitute
C0_Escape
C0_FileSeparator
C0_GroupSeparator
C0_RecordSeparator
C0_UnitSeparator
)

48
ansi/c1.go Normal file
View File

@ -0,0 +1,48 @@
package ansi
// C1 represents a list of C1 control codes.
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
type C1 byte; const (
C1_PaddingCharacter C1 = iota + 128
C1_HighOctetPreset
C1_BreakPermittedHere
C1_NoBreakHere
C1_Index
C1_NextLine
C1_StartOfSelectedArea
C1_EndOfSelectedArea
C1_CharacterTabSet
C1_CharacterTabWithJustification
C1_LineTabSet
C1_PartialLineForward
C1_PartialLineBackward
C1_ReverseLineFeed
C1_SingleShift2
C1_SingleShift3
C1_DeviceControlString
C1_PrivateUse1
C1_PrivateUse2
C1_SetTransmitState
C1_CancelCharacter
C1_MessageWaiting
C1_StartOfProtectedArea
C1_EndOfProtectedArea
C1_StartOfString
C1_SingleGraphicCharacterIntroducer
C1_SingleCharacterIntroducer
C1_ControlSequenceIntroducer
C1_StringTerminator
C1_OperatingSystemCommand
C1_PrivacyMessage
C1_ApplicationProgramCommand
)
// Is checks if a byte is equal to a C0 code.
func (code C0) Is (test byte) bool {
return test == byte(code)
}
// Is checks if a byte is equal to a C1 code.
func (code C1) Is (test byte) bool {
return byte(code) == test
}

90
ansi/color.go Normal file
View File

@ -0,0 +1,90 @@
package ansi
import "image/color"
var _ color.Color = Color(0)
// Color represents a 3, 4, or 8-Bit ansi color.
type Color byte; const (
// Dim/standard colors
ColorBlack Color = iota
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
// Bright colors
ColorBrightBlack
ColorBrightRed
ColorBrightGreen
ColorBrightYellow
ColorBrightBlue
ColorBrightMagenta
ColorBrightCyan
ColorBrightWhite
// 216 cube colors (16 - 231)
// 24 grayscale colors (232 - 255)
)
// Is16 returns whether the color is a dim or bright color, and can be assigned
// to a theme palette.
func (c Color) Is16 () bool {
return c.IsDim() || c.IsBright()
}
// IsDim returns whether the color is dim.
func (c Color) IsDim () bool {
return c < 8
}
// IsBright returns whether the color is bright.
func (c Color) IsBright () bool {
return c >= 8 && c < 16
}
// IsCube returns whether the color is part of the 6x6x6 cube.
func (c Color) IsCube () bool {
return c >= 16 && c < 232
}
// IsGrayscale returns whether the color grayscale.
func (c Color) IsGrayscale () bool {
return c >= 232 && c <= 255
}
// RGB returns the 8 bit RGB values of the color as a color.RGBA value.
func (c Color) RGB () (out color.RGBA) {
switch {
case c.Is16():
// each bit is a color component
out.R = 0xFF * uint8((c & 0x1) >> 0)
out.G = 0xFF * uint8((c & 0x2) >> 1)
out.B = 0xFF * uint8((c & 0x4) >> 3)
// dim if color is in the dim range
if c & 0x8 > 0 { out.R >>= 1; out.G >>= 1; out.B >>= 1 }
case c.IsCube():
index := int(c - 16)
out.R = uint8((((index / 36) % 6) * 255) / 5)
out.G = uint8((((index / 6) % 6) * 255) / 5)
out.B = uint8((((index ) % 6) * 255) / 5)
case c.IsGrayscale():
out.R = uint8(((int(c) - 232) * 255) / 23)
out.G = out.R
out.B = out.R
}
out.A = 0xFF
return
}
// RGBA fulfills the color.Color interface.
func (c Color) RGBA () (r, g, b, a uint32) {
return c.RGB().RGBA()
}

18
ansi/csi.go Normal file
View File

@ -0,0 +1,18 @@
package ansi
// CSI represents a list of CSI sequences that have no parameters.
type CSI int; const (
CSI_AuxPortOn CSI = iota
CSI_AuxPortOff
CSI_DeviceStatusReport
CSI_SaveCursorPosition
CSI_RestoreCursorPosition
CSI_ShowCursor
CSI_HideCursor
CSI_EnableReportingFocus
CSI_DisableReportingFocus
CSI_EnableAlternativeBuffer
CSI_DisableAlternativeBuffer
CSI_EnableBracketedPasteMode
CSI_DisableBracketedPasteMode
)

157
ansi/decoder.go Normal file
View File

@ -0,0 +1,157 @@
package ansi
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)
// 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)
// 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
csiParameter []byte
csiIdentifier byte
}
type decodeState int; const (
decodeStateText decodeState = iota
decodeStateAwaitC1
decodeStateGatherCSI
)
func (decoder *Decoder) Write (buffer []byte) (wrote int, err error) {
wrote = len(buffer)
for len(buffer) > 0 {
switch decoder.state {
case decodeStateText:
if buffer[0] == byte(C0_Escape) {
// 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:
// 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
decoder.state = decodeStateGatherCSI
decoder.csiParameter = nil
decoder.csiIdentifier = 0
} else {
// process C1 control code
if decoder.OnC1 != nil {
decoder.OnC1(C1(buffer[0]))
}
}
buffer = buffer[1:]
case decodeStateGatherCSI:
if buffer[0] < 0x30 || buffer[0] > 0x3F {
decoder.csiIdentifier = buffer[0]
decoder.processCSI()
} else {
decoder.csiParameter = append (
decoder.csiParameter,
buffer[0])
}
buffer = buffer[1:]
}
}
return
}
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) processCSI () {
// TODO: analyze CSI parameter and id
}

97
ansi/sgr.go Normal file
View File

@ -0,0 +1,97 @@
package ansi
// SGR represents a list of Select Graphic Rendition parameters.
type SGR int; const (
SGR_Normal SGR = iota
SGR_Bold
SGR_Dim
SGR_Italic
SGR_Underline
SGR_SlowBlink
SGR_RapidBlink
SGR_Reverse
SGR_Conceal
SGR_Strike
SGR_FontPrimary
SGR_Font1
SGR_Font2
SGR_Font3
SGR_Font4
SGR_Font5
SGR_Font6
SGR_Font7
SGR_Font8
SGR_Font9
SGR_FontFraktur
SGR_DoubleUnderline
SGR_NormalIntensity
SGR_NeitherItalicNorBlackletter
SGR_NotUnderlined
SGR_NotBlinking
SGR_PorportionalSpacing
SGR_NotReversed
SGR_NotCrossedOut
SGR_ForegroundColorBlack
SGR_ForegroundColorRed
SGR_ForegroundColorGreen
SGR_ForegroundColorYellow
SGR_ForegroundColorBlue
SGR_ForegroundColorMagenta
SGR_ForegroundColorCyan
SGR_ForegroundColorWhite
SGR_ForegroundColor
SGR_ForegroundColorDefault
SGR_BackgroundColorBlack
SGR_BackgroundColorRed
SGR_BackgroundColorGreen
SGR_BackgroundColorYellow
SGR_BackgroundColorBlue
SGR_BackgroundColorMagenta
SGR_BackgroundColorCyan
SGR_BackgroundColorWhite
SGR_BackgroundColor
SGR_BackgroundColorDefault
SGR_DisablePorportionalSpacing
SGR_Framed
SGR_Encircled
SGR_Overlined
SGR_NeitherFramedNorEncircled
SGR_NotOverlined
SGR_UnderlineColor
SGR_UnderlineColorDefault
SGR_IdeogramUnderline
SGR_IdeogramDoubleUnderline
SGR_IdeogramOverline
SGR_IdeogramDoubleOverline
SGR_IdeogramStressMarking
SGR_NoIdeogramAttributes
SGR_Superscript
SGR_Subscript
SGR_NeitherSuperscriptNorSubscript
SGR_ForegroundColorBrightBlack
SGR_ForegroundColorBrightRed
SGR_ForegroundColorBrightGreen
SGR_ForegroundColorBrightYellow
SGR_ForegroundColorBrightBlue
SGR_ForegroundColorBrightMagenta
SGR_ForegroundColorBrightCyan
SGR_ForegroundColorBrightWhite
SGR_BackgroundColorBrightBlack
SGR_BackgroundColorBrightRed
SGR_BackgroundColorBrightGreen
SGR_BackgroundColorBrightYellow
SGR_BackgroundColorBrightBlue
SGR_BackgroundColorBrightMagenta
SGR_BackgroundColorBrightCyan
SGR_BackgroundColorBrightWhite
)

View File

@ -17,6 +17,11 @@ type gridCell struct {
clean bool
}
func (cell *gridCell) initColor () {
cell.background = tomo.ColorBackground
cell.foreground = tomo.ColorForeground
}
type gridBuffer struct {
cells []gridCell
stride int
@ -35,6 +40,8 @@ type Grid struct {
cellWidth int
cellHeight int
cursor image.Point
face font.Face
config config.Wrapped
theme theme.Wrapped
@ -51,6 +58,14 @@ func NewGrid () (element *Grid) {
return
}
func (element *Grid) OnResize (callback func ()) {
element.onResize = callback
}
func (element *Grid) Write (data []byte) (wrote int, err error) {
// TODO process ansi escape codes etx
}
func (element *Grid) HandleMouseDown (x, y int, button input.Button) {
}
@ -60,12 +75,10 @@ func (element *Grid) HandleMouseUp (x, y int, button input.Button) {
}
func (element *Grid) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
// TODO we need to grab shift ctrl c for copying text
}
func (element *Grid) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
}
func (element *Grid) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
// SetTheme sets the element's theme.
func (element *Grid) SetTheme (new tomo.Theme) {
@ -93,13 +106,33 @@ func (element *Grid) alloc () bool {
height == len(element.cells) / element.stride
if unchanged { return false }
// TODO: attempt to wrap text
oldCells := element.cells
oldWidth := element.stride
oldHeight := len(element.cells) / element.stride
heightLarger := height < oldHeight
element.stride = width
element.cells = make([]gridCell, width * height)
for index := range element.cells {
element.cells[index].background = tomo.ColorBackground
element.cells[index].foreground = tomo.ColorForeground
// TODO: attempt to wrap text?
if heightLarger {
for index := range element.cells[oldHeight * width:] {
element.cells[index].initColor()
}}
commonHeight := height
if heightLarger { commonHeight = oldHeight }
for index := range element.cells[:commonHeight * width] {
x := index % width
if x < oldWidth {
element.cells[index] = oldCells[x + index / oldWidth]
} else {
element.cells[index].initColor()
}
}
if element.onResize != nil { element.onResize() }
return true
}
@ -130,5 +163,5 @@ func (element *Grid) drawAndPush () {
}
func (element *Grid) draw (force bool) image.Rectangle {
}