From f21a41982ef647e7e8ab35b36b5c216e982850e8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 6 Apr 2023 13:38:47 -0400 Subject: [PATCH] ANSI escape code decoder wip --- ansi/c0.go | 38 ++++++++++++ ansi/c1.go | 48 +++++++++++++++ ansi/color.go | 90 +++++++++++++++++++++++++++ ansi/csi.go | 18 ++++++ ansi/decoder.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++ ansi/sgr.go | 97 +++++++++++++++++++++++++++++ elements/grid.go | 51 ++++++++++++--- 7 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 ansi/c0.go create mode 100644 ansi/c1.go create mode 100644 ansi/color.go create mode 100644 ansi/csi.go create mode 100644 ansi/decoder.go create mode 100644 ansi/sgr.go diff --git a/ansi/c0.go b/ansi/c0.go new file mode 100644 index 0000000..6d37ff2 --- /dev/null +++ b/ansi/c0.go @@ -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 +) diff --git a/ansi/c1.go b/ansi/c1.go new file mode 100644 index 0000000..2448367 --- /dev/null +++ b/ansi/c1.go @@ -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 +} diff --git a/ansi/color.go b/ansi/color.go new file mode 100644 index 0000000..5bb052f --- /dev/null +++ b/ansi/color.go @@ -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() +} diff --git a/ansi/csi.go b/ansi/csi.go new file mode 100644 index 0000000..417a2db --- /dev/null +++ b/ansi/csi.go @@ -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 +) diff --git a/ansi/decoder.go b/ansi/decoder.go new file mode 100644 index 0000000..2dd8dce --- /dev/null +++ b/ansi/decoder.go @@ -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 +} diff --git a/ansi/sgr.go b/ansi/sgr.go new file mode 100644 index 0000000..1aa6197 --- /dev/null +++ b/ansi/sgr.go @@ -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 +) diff --git a/elements/grid.go b/elements/grid.go index 4664938..9dbb7b4 100644 --- a/elements/grid.go +++ b/elements/grid.go @@ -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 { - + }