Use Esc[ bytes for ansi detection+move ansi to its own package

This commit is contained in:
bloeys
2022-07-22 19:58:20 +04:00
parent 0d2e747739
commit b6f39bd1ba
2 changed files with 18 additions and 16 deletions

428
ansi/ansi.go Executable file
View File

@ -0,0 +1,428 @@
package ansi
import (
"bytes"
"fmt"
"github.com/bloeys/gglm/gglm"
)
type CSIType int
const (
CSIType_Unknown CSIType = iota
// Moves the cursor n (default 1) cells in the given direction. This has no effect if the cursor is at the edge of the screen
CSIType_CUU // Cursor Up
CSIType_CUD // Cursor Down
CSIType_CUF // Cursor Forward
CSIType_CUB // Cursor Back
// Moves cursor to beginning of the line n (default 1) lines down/up
CSIType_CNL // Cursor Next Line
CSIType_CPL // Cursor Previous Line
CSIType_CHA // Cursor Horizontal Absolute. Moves the cursor to column n (default 1)
CSIType_CUP // Cursor Position. Moves the cursor to row n, column m. The values are 1-based, and default to 1 (top left corner) if omitted
// Erase in Display. Clears part of the screen.
// If n is 0 (or missing) clear from cursor to end of screen.
// If n is 1, clear from cursor to beginning of the screen.
// If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS).
// If n is 3, clear entire screen and delete all lines saved in the scrollback buffer.
CSIType_ED
// Erase in Line. Erases part of the line.
// If n is 0 (or missing), clear from cursor to the end of the line.
// If n is 1, clear from cursor to beginning of the line.
// If n is 2, clear entire line. Cursor position does not change.
CSIType_EL
// Scroll Up. Scroll whole page up by n (default 1) lines. New lines are added at the bottom
CSIType_SU
// Scroll Down. Scroll whole page down by n (default 1) lines. New lines are added at the top
CSIType_SD
// Horizontal Vertical Position. Same as CUP, but counts as a format effector function (like CR or LF) rather than an editor function (like CUD or CNL)
CSIType_HVP
// Select Graphic Rendition. Sets colors and style of the characters following this code
CSIType_SGR
// Device Status Report. Reports the cursor position (CPR) by transmitting ESC[n;mR, where n is the row and m is the column
CSIType_DSR
)
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
// For Control Sequence Introducer (CSI) commands, `ESC[` is followed by:
// Zero or more "parameter bytes" in the range 0x300x3F.
// Zero or more "intermediate bytes" in the range 0x200x2F.
// One "final byte" in the range 0x400x7E.
const (
AnsiCsiParamBytesStart = 0x30
AnsiCsiParamBytesEnd = 0x3F
AnsiCsiIntermBytesStart = 0x20
AnsiCsiIntermBytesEnd = 0x2F
AnsiCsiFinalBytesStart = 0x40
AnsiCsiFinalBytesEnd = 0x7E
)
const (
Ansi_Fg_Black = 30
Ansi_Fg_Red = 31
Ansi_Fg_Green = 32
Ansi_Fg_Yellow = 33
Ansi_Fg_Blue = 34
Ansi_Fg_Magenta = 35
Ansi_Fg_Cyan = 36
Ansi_Fg_White = 37
Ansi_Fg_Gray = 90
Ansi_Fg_Bright_Red = 91
Ansi_Fg_Bright_Green = 92
Ansi_Fg_Bright_Yellow = 93
Ansi_Fg_Bright_Blue = 94
Ansi_Fg_Bright_Magenta = 95
Ansi_Fg_Bright_Cyan = 96
Ansi_Fg_Bright_White = 97
Ansi_Bg_Black = 40
Ansi_Bg_Red = 41
Ansi_Bg_Green = 42
Ansi_Bg_Yellow = 43
Ansi_Bg_Blue = 44
Ansi_Bg_Magenta = 45
Ansi_Bg_Cyan = 46
Ansi_Bg_White = 47
Ansi_Bg_Gray = 100
Ansi_Bg_Bright_Red = 101
Ansi_Bg_Bright_Green = 102
Ansi_Bg_Bright_Yellow = 103
Ansi_Bg_Bright_Blue = 104
Ansi_Bg_Bright_Magenta = 105
Ansi_Bg_Bright_Cyan = 106
Ansi_Bg_Bright_White = 107
)
var (
AnsiEscByte = byte('\x1b')
// AnsiEscStringBytes = []byte{'\\', 'x', '1', 'b'} // represents the string: \x1b
AnsiCSIBytes = []byte{'\x1b', '['}
AnsiCSIBytesLen = len(AnsiCSIBytes)
// AnsiCSIStringBytes = []byte{'\\', 'x', '1', 'b', '['} // represents the string: \x1b[
// AnsiCSIStringBytesLen = len(AnsiCSIStringBytes)
)
type AnsiCodeOptions int64
const (
AnsiCodeOptions_ColorFg AnsiCodeOptions = 1 << iota
AnsiCodeOptions_ColorBg
AnsiCodeOptions_ColorDefault
AnsiCodeOptions_CursorOffset
AnsiCodeOptions_CursorAbs
AnsiCodeOptions_LineOffset
AnsiCodeOptions_LineAbs
AnsiCodeOptions_ScrollOffset
// This is at the bottom so iota starts at 0
AnsiCodeOptions_Unknown AnsiCodeOptions = 0
)
type AnsiCodeInfo struct {
Type CSIType
// When type is CSIType_SGR and the code is reset info1.X=-1
Info1 gglm.Vec4
Options AnsiCodeOptions
}
func NextAnsiCode(arr []byte) (index int, code []byte) {
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
// For Control Sequence Introducer (CSI) commands, `ESC[` is followed by:
// Zero or more "parameter bytes" in the range 0x300x3F.
// Zero or more "intermediate bytes" in the range 0x200x2F.
// One "final byte" in the range 0x400x7E.
const paramBytesRegion = 0
const intermBytesRegion = 1
startOffset := 0
for startOffset < len(arr)-1 {
ansiEscIndex := bytes.Index(arr[startOffset:], AnsiCSIBytes)
if ansiEscIndex == -1 {
return -1, nil
}
ansiEscIndex += startOffset
startOffset = ansiEscIndex + AnsiCSIBytesLen
// Now that we have found an ESC[, to parse the sequence we expect bytes in a specific order
// and a specific range. That is, a valid "paramter bytes" character in the interm bytes region
// is considered an invalid char and will invalidate the sequence
finalByteIndex := -1
region := paramBytesRegion
for i := ansiEscIndex + AnsiCSIBytesLen; i < len(arr); i++ {
b := arr[i]
if region == paramBytesRegion {
if b >= AnsiCsiParamBytesStart && b <= AnsiCsiParamBytesEnd {
continue
}
if b >= AnsiCsiIntermBytesStart && b <= AnsiCsiIntermBytesEnd {
region = intermBytesRegion
continue
}
if b >= AnsiCsiFinalBytesStart && b <= AnsiCsiFinalBytesEnd {
finalByteIndex = i
break
}
break
} else {
if b >= AnsiCsiIntermBytesStart && b <= AnsiCsiIntermBytesEnd {
continue
}
if b >= AnsiCsiFinalBytesStart && b <= AnsiCsiFinalBytesEnd {
finalByteIndex = i
break
}
break
}
}
//If we fail to parse this sequence we continue to search ahead in the string
if finalByteIndex == -1 {
continue
}
return ansiEscIndex, arr[ansiEscIndex : finalByteIndex+1]
}
return -1, nil
}
func InfoFromAnsiCode(code []byte) (info AnsiCodeInfo) {
codeLen := len(code)
if codeLen < AnsiCSIBytesLen+1 {
return info
}
finalByte := code[codeLen-1]
args := code[AnsiCSIBytesLen : codeLen-1]
// @TODO finish parsing and filling struct
switch finalByte {
case 'm':
info.Type = CSIType_SGR
ParseSGRArgs(&info, args)
case 'A':
info.Type = CSIType_CUU
case 'B':
info.Type = CSIType_CUD
case 'C':
info.Type = CSIType_CUF
case 'D':
info.Type = CSIType_CUB
case 'E':
info.Type = CSIType_CNL
case 'F':
info.Type = CSIType_CPL
case 'G':
info.Type = CSIType_CHA
case 'H':
info.Type = CSIType_CUP
case 'J':
info.Type = CSIType_ED
case 'K':
info.Type = CSIType_EL
case 'S':
info.Type = CSIType_SU
case 'T':
info.Type = CSIType_SD
case 'f':
info.Type = CSIType_HVP
// case 'n':
// if code[codeLen-2] == '6' {
// info.Type = CSIType_DSR
// args = code[AnsiCSIBytesLen : codeLen-2]
// }
}
return info
}
func ParseSGRArgs(info *AnsiCodeInfo, args []byte) {
// @TODO should we trim spaces?
splitArgs := bytes.Split(args, []byte{';'})
for _, a := range splitArgs {
if len(a) == 0 || a[0] == byte('0') {
info.Info1.SetX(-1)
info.Options |= AnsiCodeOptions_ColorFg
continue
}
intCode := getSgrIntCodeFromBytes(a)
if intCode >= 30 && intCode <= 37 || intCode >= 90 && intCode <= 97 {
info.Info1 = ColorFromSgrCode(intCode)
info.Options |= AnsiCodeOptions_ColorFg
continue
}
if intCode >= 40 && intCode <= 47 || intCode >= 100 && intCode <= 107 {
info.Info1 = ColorFromSgrCode(intCode)
info.Options |= AnsiCodeOptions_ColorBg
continue
}
// @TODO Support bold/underline etc
// @TODO Support 256 and RGB colors
panic("Code not supported yet: " + fmt.Sprint(intCode))
}
}
func getSgrIntCodeFromBytes(bs []byte) (code int) {
mul := 1
for i := 1; i < len(bs); i++ {
mul *= 10
}
for i := 0; i < len(bs); i++ {
b := bs[i]
switch b {
case '1':
code += 1 * mul
case '2':
code += 2 * mul
case '3':
code += 3 * mul
case '4':
code += 4 * mul
case '5':
code += 5 * mul
case '6':
code += 6 * mul
case '7':
code += 7 * mul
case '8':
code += 8 * mul
case '9':
code += 9 * mul
}
mul /= 10
}
return code
}
func ColorFromSgrCode(code int) gglm.Vec4 {
switch code {
//Foreground and background
case Ansi_Bg_Black:
fallthrough
case Ansi_Fg_Black:
return gglm.Vec4{}
case Ansi_Bg_Red:
fallthrough
case Ansi_Fg_Red:
return gglm.Vec4{Data: [4]float32{0.7, 0, 0, 1}}
case Ansi_Bg_Green:
fallthrough
case Ansi_Fg_Green:
return gglm.Vec4{Data: [4]float32{0, 0.7, 0, 1}}
case Ansi_Bg_Yellow:
fallthrough
case Ansi_Fg_Yellow:
return gglm.Vec4{Data: [4]float32{0.7, 0.7, 0, 1}}
case Ansi_Bg_Blue:
fallthrough
case Ansi_Fg_Blue:
return gglm.Vec4{Data: [4]float32{0, 0, 0.7, 1}}
case Ansi_Bg_Magenta:
fallthrough
case Ansi_Fg_Magenta:
return gglm.Vec4{Data: [4]float32{0.7, 0, 0.7, 1}}
case Ansi_Bg_Cyan:
fallthrough
case Ansi_Fg_Cyan:
return gglm.Vec4{Data: [4]float32{0, 0.66, 0.66, 1}}
case Ansi_Bg_White:
fallthrough
case Ansi_Fg_White:
return gglm.Vec4{Data: [4]float32{0.8, 0.8, 0.8, 1}}
case Ansi_Bg_Gray:
fallthrough
case Ansi_Fg_Gray:
return gglm.Vec4{Data: [4]float32{0.7, 0.7, 0.7, 1}}
//Bright foreground and background
case Ansi_Bg_Bright_Red:
fallthrough
case Ansi_Fg_Bright_Red:
return gglm.Vec4{Data: [4]float32{1, 0, 0, 1}}
case Ansi_Bg_Bright_Green:
fallthrough
case Ansi_Fg_Bright_Green:
return gglm.Vec4{Data: [4]float32{0, 1, 0, 1}}
case Ansi_Bg_Bright_Yellow:
fallthrough
case Ansi_Fg_Bright_Yellow:
return gglm.Vec4{Data: [4]float32{1, 1, 0, 1}}
case Ansi_Bg_Bright_Blue:
fallthrough
case Ansi_Fg_Bright_Blue:
return gglm.Vec4{Data: [4]float32{0, 0, 1, 1}}
case Ansi_Bg_Bright_Magenta:
fallthrough
case Ansi_Fg_Bright_Magenta:
return gglm.Vec4{Data: [4]float32{1, 0, 1, 1}}
case Ansi_Bg_Bright_Cyan:
fallthrough
case Ansi_Fg_Bright_Cyan:
return gglm.Vec4{Data: [4]float32{0, 1, 1, 1}}
case Ansi_Bg_Bright_White:
fallthrough
case Ansi_Fg_Bright_White:
return gglm.Vec4{Data: [4]float32{1, 1, 1, 1}}
}
panic("Invalid ansi code: " + fmt.Sprint(code))
}