New ansi system to support anything later+ 8/16 bit color

This commit is contained in:
bloeys
2022-07-17 01:29:48 +04:00
parent 70cf0e6008
commit e2feafa89d
2 changed files with 341 additions and 105 deletions

311
ansi.go
View File

@ -7,6 +7,53 @@ import (
"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.
@ -21,11 +68,75 @@ const (
AnsiCsiFinalBytesEnd = 0x7E
)
var (
AnsiEscBytes = []byte{'\\', 'x', '1', 'b', '['}
AnsiEscBytesLen = len(AnsiEscBytes)
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 (
// represents the string: \x1b
AnsiEscBytes = []byte{'\\', 'x', '1', 'b'}
// represents the string: \x1b[
AnsiCSIBytes = []byte{'\\', 'x', '1', 'b', '['}
AnsiCSIBytesLen = len(AnsiCSIBytes)
)
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
@ -40,19 +151,19 @@ func NextAnsiCode(arr []byte) (index int, code []byte) {
startOffset := 0
for startOffset < len(arr)-1 {
ansiEscIndex := bytes.Index(arr[startOffset:], AnsiEscBytes)
ansiEscIndex := bytes.Index(arr[startOffset:], AnsiCSIBytes)
if ansiEscIndex == -1 {
return -1, nil
}
ansiEscIndex += startOffset
startOffset = ansiEscIndex + AnsiEscBytesLen
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 + AnsiEscBytesLen; i < len(arr); i++ {
for i := ansiEscIndex + AnsiCSIBytesLen; i < len(arr); i++ {
b := arr[i]
@ -101,40 +212,212 @@ func NextAnsiCode(arr []byte) (index int, code []byte) {
return -1, nil
}
func fgColorFromAnsiCode(code int) gglm.Vec4 {
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.5, 0, 0, 1}}
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.5, 0, 1}}
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.5, 0.5, 0, 1}}
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.5, 1}}
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.5, 0, 0.5, 1}}
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_Fg_Gray:
return gglm.Vec4{Data: [4]float32{0.5, 0.5, 0.5, 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}}

135
main.go
View File

@ -7,7 +7,6 @@ import (
"os"
"os/exec"
"runtime/pprof"
"strconv"
"strings"
"time"
"unicode/utf8"
@ -291,24 +290,24 @@ func (p *program) MainUpdate() {
p.DeleteNextChar()
}
//Draw textBuf
// @TODO cmds should be printed with only syntax highlighting
// Draw textBuf
v1, v2 := p.textBuf.Views()
from := clamp(p.scrollPos, 0, int64(len(v1)-1))
to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v1)-1))
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(bytesToRunes(v1[from:to]), *gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0)).Data
// p.lastCmdCharPos.Data = p.GlyphRend.DrawTextOpenGLAbs(v1[from:to], gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0), &p.Settings.DefaultColor).Data
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v1[from:to], *gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0)).Data
if p.scrollPos >= int64(len(v1)) {
from := clamp(p.scrollPos-int64(len(v1)), 0, int64(len(v2)-1))
to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v2)-1))
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(bytesToRunes(v2[from:to]), *p.lastCmdCharPos).Data
p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(v2[from:to], *p.lastCmdCharPos).Data
}
sepLinePos.Data = p.lastCmdCharPos.Data
//Draw cmd buf
// Draw cmd buf
p.lastCmdCharPos.SetX(0)
p.lastCmdCharPos.AddY(-p.GlyphRend.Atlas.LineHeight)
p.lastCmdCharPos.Data = p.SyntaxHighlightAndDraw(p.cmdBuf[:p.cmdBufLen], *p.lastCmdCharPos).Data
@ -337,104 +336,58 @@ func bytesToRunes(b []byte) []rune {
return out
}
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
func (p *program) DrawTextAnsiCodes(bs []byte, pos gglm.Vec3) gglm.Vec3 {
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
)
func (p *program) DrawTextAnsiCodes(text []rune, pos gglm.Vec3) gglm.Vec3 {
// const ansiEsc = '\x1b'
// const ansiChar1 = '\x1b'
startIndex := 0
startPos := pos.Clone()
currColor := p.Settings.DefaultColor
for i := 0; i < len(text); i++ {
r := text[i]
draw := func(rs []rune) {
if r == '\n' {
pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, &currColor).Data
pos.SetX(startPos.X())
pos.AddY(-p.GlyphRend.Atlas.LineHeight)
startIndex = i + 1
continue
}
startIndex := 0
for i := 0; i < len(rs); i++ {
if r != '\\' || len(text)-i < 6 {
continue
}
r := rs[i]
if text[i+1] == 'x' && text[i+2] == '1' && text[i+3] == 'b' && text[i+4] == '[' {
pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, &currColor).Data
mIndex := -1
loopStart := i + 5
for i := loopStart; i < len(text) && i < loopStart+10; i++ {
r := text[i]
if r == 'm' {
mIndex = i
break
}
}
codeRunes := text[i+5 : mIndex]
code, err := strconv.Atoi(string(codeRunes))
if err != nil {
println("Invalid code runes:", string(codeRunes))
// @PERF We could probably use bytes.IndexByte here
if r == '\n' {
pos.Data = p.GlyphRend.DrawTextOpenGLAbs(rs[startIndex:i], &pos, &currColor).Data
pos.SetX(startPos.X())
pos.AddY(-p.GlyphRend.Atlas.LineHeight)
startIndex = i + 1
continue
}
}
if mIndex > -1 {
startIndex = mIndex + 1
} else {
startIndex = i
}
if code == 0 {
currColor = p.Settings.DefaultColor
} else {
currColor.Data = fgColorFromAnsiCode(code).Data
}
if startIndex < len(rs) {
pos.Data = p.GlyphRend.DrawTextOpenGLAbs(rs[startIndex:], &pos, &currColor).Data
}
}
if startIndex < len(text) {
p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:], &pos, &currColor)
for {
index, code := NextAnsiCode(bs)
if index == -1 {
draw(bytesToRunes(bs))
break
}
// Draw text before the code
before := bytesToRunes(bs[:index])
draw(before)
//Apply code
info := InfoFromAnsiCode(code)
if info.Options&AnsiCodeOptions_ColorFg != 0 {
if info.Info1.X() == -1 {
currColor = p.Settings.DefaultColor
} else {
currColor = info.Info1
}
}
// Advance beyond the code chars
bs = bs[index+len(code):]
}
return pos