mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
Use Esc[ bytes for ansi detection+move ansi to its own package
This commit is contained in:
428
ansi/ansi.go
Executable file
428
ansi/ansi.go
Executable 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 0x30–0x3F.
|
||||
// Zero or more "intermediate bytes" in the range 0x20–0x2F.
|
||||
// One "final byte" in the range 0x40–0x7E.
|
||||
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 0x30–0x3F.
|
||||
// Zero or more "intermediate bytes" in the range 0x20–0x2F.
|
||||
// One "final byte" in the range 0x40–0x7E.
|
||||
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user