vibetunnel/linux/pkg/terminal/ansi_parser.go
2025-06-20 16:35:28 +02:00

223 lines
5.1 KiB
Go

package terminal
import (
"unicode/utf8"
)
// AnsiParser implements a state machine for parsing ANSI escape sequences
type AnsiParser struct {
state parserState
intermediate []byte
params []int
currentParam int
oscData []byte
// Callbacks
OnPrint func(rune)
OnExecute func(byte)
OnCsi func(params []int, intermediate []byte, final byte)
OnOsc func(params [][]byte)
OnEscape func(intermediate []byte, final byte)
}
type parserState int
const (
stateGround parserState = iota
stateEscape
stateEscapeIntermediate
stateCsiEntry
stateCsiParam
stateCsiIntermediate
stateCsiIgnore
stateOscString
stateDcsEntry
stateDcsParam
stateDcsIntermediate
stateDcsPassthrough
stateDcsIgnore
)
// NewAnsiParser creates a new ANSI escape sequence parser
func NewAnsiParser() *AnsiParser {
return &AnsiParser{
state: stateGround,
intermediate: make([]byte, 0, 2),
params: make([]int, 0, 16),
}
}
// Parse processes input bytes through the ANSI state machine
func (p *AnsiParser) Parse(data []byte) {
for i := 0; i < len(data); {
b := data[i]
switch p.state {
case stateGround:
if b == 0x1b { // ESC
p.state = stateEscape
i++
} else if b < 0x20 { // C0 control codes
if p.OnExecute != nil {
p.OnExecute(b)
}
i++
} else if b < 0x80 { // ASCII printable
if p.OnPrint != nil {
p.OnPrint(rune(b))
}
i++
} else { // UTF-8 or extended ASCII
r, size := utf8.DecodeRune(data[i:])
if r != utf8.RuneError && p.OnPrint != nil {
p.OnPrint(r)
}
i += size
}
case stateEscape:
p.intermediate = p.intermediate[:0]
if b >= 0x20 && b <= 0x2f { // Intermediate bytes
p.intermediate = append(p.intermediate, b)
p.state = stateEscapeIntermediate
} else if b == '[' { // CSI
p.params = p.params[:0]
p.currentParam = 0
p.state = stateCsiEntry
} else if b == ']' { // OSC
p.oscData = p.oscData[:0]
p.state = stateOscString
} else if b >= 0x30 && b <= 0x7e { // Final byte
if p.OnEscape != nil {
p.OnEscape(p.intermediate, b)
}
p.state = stateGround
} else {
p.state = stateGround
}
i++
case stateEscapeIntermediate:
if b >= 0x20 && b <= 0x2f { // More intermediate bytes
p.intermediate = append(p.intermediate, b)
} else if b >= 0x30 && b <= 0x7e { // Final byte
if p.OnEscape != nil {
p.OnEscape(p.intermediate, b)
}
p.state = stateGround
} else {
p.state = stateGround
}
i++
case stateCsiEntry:
if b >= '0' && b <= '9' { // Parameter digit
p.currentParam = int(b - '0')
p.state = stateCsiParam
} else if b == ';' { // Parameter separator
p.params = append(p.params, 0)
} else if b >= 0x20 && b <= 0x2f { // Intermediate bytes
p.intermediate = append(p.intermediate, b)
p.state = stateCsiIntermediate
} else if b >= 0x40 && b <= 0x7e { // Final byte
if p.OnCsi != nil {
p.OnCsi(p.params, p.intermediate, b)
}
p.state = stateGround
} else {
p.state = stateCsiIgnore
}
i++
case stateCsiParam:
if b >= '0' && b <= '9' { // More digits
p.currentParam = p.currentParam*10 + int(b-'0')
} else if b == ';' { // Parameter separator
p.params = append(p.params, p.currentParam)
p.currentParam = 0
} else if b >= 0x20 && b <= 0x2f { // Intermediate bytes
p.params = append(p.params, p.currentParam)
p.intermediate = append(p.intermediate, b)
p.state = stateCsiIntermediate
} else if b >= 0x40 && b <= 0x7e { // Final byte
p.params = append(p.params, p.currentParam)
if p.OnCsi != nil {
p.OnCsi(p.params, p.intermediate, b)
}
p.state = stateGround
} else {
p.state = stateCsiIgnore
}
i++
case stateCsiIntermediate:
if b >= 0x20 && b <= 0x2f { // More intermediate bytes
p.intermediate = append(p.intermediate, b)
} else if b >= 0x40 && b <= 0x7e { // Final byte
if p.OnCsi != nil {
p.OnCsi(p.params, p.intermediate, b)
}
p.state = stateGround
} else {
p.state = stateCsiIgnore
}
i++
case stateCsiIgnore:
if b >= 0x40 && b <= 0x7e { // Wait for final byte
p.state = stateGround
}
i++
case stateOscString:
if b == 0x07 { // BEL terminates OSC
if p.OnOsc != nil {
// Parse OSC data
p.parseOscData()
}
p.state = stateGround
} else if b == 0x1b && i+1 < len(data) && data[i+1] == '\\' { // ESC \ also terminates
if p.OnOsc != nil {
p.parseOscData()
}
p.state = stateGround
i++ // Skip the backslash
} else {
p.oscData = append(p.oscData, b)
}
i++
default:
p.state = stateGround
i++
}
}
}
// parseOscData splits OSC data into parameters
func (p *AnsiParser) parseOscData() {
params := make([][]byte, 0)
start := 0
for i, b := range p.oscData {
if b == ';' {
params = append(params, p.oscData[start:i])
start = i + 1
}
}
if start < len(p.oscData) {
params = append(params, p.oscData[start:])
}
p.OnOsc(params)
}
// Reset resets the parser to ground state
func (p *AnsiParser) Reset() {
p.state = stateGround
p.intermediate = p.intermediate[:0]
p.params = p.params[:0]
p.currentParam = 0
p.oscData = p.oscData[:0]
}