mirror of
https://github.com/bloeys/nterm.git
synced 2025-12-29 06:28:20 +00:00
444 lines
11 KiB
Go
Executable File
444 lines
11 KiB
Go
Executable File
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 AnsiCodePayloadType int32
|
||
|
||
const (
|
||
AnsiCodePayloadType_Unknown AnsiCodePayloadType = iota
|
||
AnsiCodePayloadType_ColorFg
|
||
AnsiCodePayloadType_ColorBg
|
||
AnsiCodePayloadType_Reset
|
||
|
||
AnsiCodePayloadType_CursorOffset
|
||
AnsiCodePayloadType_CursorAbs
|
||
AnsiCodePayloadType_LineOffset
|
||
AnsiCodePayloadType_LineAbs
|
||
AnsiCodePayloadType_ScrollOffset
|
||
)
|
||
|
||
func (a AnsiCodePayloadType) HasOption(opt AnsiCodePayloadType) bool {
|
||
return a == opt
|
||
}
|
||
|
||
type AnsiCodeInfoPayload struct {
|
||
Info gglm.Vec4
|
||
Type AnsiCodePayloadType
|
||
}
|
||
|
||
type AnsiCodeInfo struct {
|
||
Type CSIType
|
||
Payload []AnsiCodeInfoPayload
|
||
}
|
||
|
||
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
|
||
info.Payload = ParseSGRArgs(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(args []byte) (payload []AnsiCodeInfoPayload) {
|
||
|
||
// @PERF: Too many allocations here, once per code :/
|
||
payload = make([]AnsiCodeInfoPayload, 0, 1)
|
||
|
||
// @TODO should we trim spaces?
|
||
splitArgs := bytes.Split(args, []byte{';'})
|
||
for _, a := range splitArgs {
|
||
|
||
if len(a) == 0 || a[0] == byte('0') {
|
||
payload = append(payload, AnsiCodeInfoPayload{
|
||
Type: AnsiCodePayloadType_Reset,
|
||
})
|
||
continue
|
||
}
|
||
|
||
// @TODO We can't use this setup of one info field because one ansi code can have many settings.
|
||
// For example, it can set Fg+Bg at once. So we need info per option.
|
||
intCode := getSgrIntCodeFromBytes(a)
|
||
if intCode >= 30 && intCode <= 37 || intCode >= 90 && intCode <= 97 {
|
||
payload = append(payload, AnsiCodeInfoPayload{
|
||
Info: ColorFromSgrCode(intCode),
|
||
Type: AnsiCodePayloadType_ColorFg,
|
||
})
|
||
continue
|
||
}
|
||
|
||
if intCode >= 40 && intCode <= 47 || intCode >= 100 && intCode <= 107 {
|
||
payload = append(payload, AnsiCodeInfoPayload{
|
||
Info: ColorFromSgrCode(intCode),
|
||
Type: AnsiCodePayloadType_ColorBg,
|
||
})
|
||
continue
|
||
}
|
||
|
||
// @TODO Support bold/underline etc
|
||
// @TODO Support 256 and RGB colors
|
||
println("Code not supported yet: " + fmt.Sprint(intCode))
|
||
}
|
||
|
||
return payload
|
||
}
|
||
|
||
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{Data: [4]float32{0, 0, 0, 1}}
|
||
|
||
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))
|
||
}
|