162 lines
4.6 KiB
Go
162 lines
4.6 KiB
Go
package runtime
|
|
|
|
import "bytes"
|
|
|
|
// vtStreamNormalizer preserves incomplete CSI sequences across PTY reads before
|
|
// applying the SGR compatibility rewrite that vt10x still needs.
|
|
type vtStreamNormalizer struct {
|
|
pendingCSI []byte
|
|
}
|
|
|
|
func (n *vtStreamNormalizer) Normalize(data []byte) []byte {
|
|
if len(n.pendingCSI) > 0 {
|
|
combined := make([]byte, 0, len(n.pendingCSI)+len(data))
|
|
combined = append(combined, n.pendingCSI...)
|
|
combined = append(combined, data...)
|
|
data = combined
|
|
n.pendingCSI = nil
|
|
}
|
|
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]byte, 0, len(data)+32)
|
|
for i := 0; i < len(data); {
|
|
if data[i] == 0x1b {
|
|
if i+1 >= len(data) {
|
|
n.pendingCSI = append(n.pendingCSI[:0], data[i:]...)
|
|
break
|
|
}
|
|
if data[i+1] == '[' {
|
|
start := i
|
|
j := i + 2
|
|
for j < len(data) && data[j] < 0x40 {
|
|
j++
|
|
}
|
|
if j >= len(data) {
|
|
n.pendingCSI = append(n.pendingCSI[:0], data[start:]...)
|
|
break
|
|
}
|
|
// Drop CSI sequences that vt10x would misinterpret.
|
|
// Sequences with intermediate bytes (>, <, =) are private-use
|
|
// extensions (e.g. Kitty keyboard \x1b[>1u) that vt10x's CSI
|
|
// parser misroutes. Also drop ?-prefixed sequences ending in
|
|
// 'u' (\x1b[?u — Kitty keyboard query) which vt10x wrongly
|
|
// dispatches as DECRC (cursor restore), corrupting cursor state.
|
|
if shouldDropCSI(data[i+2:j], data[j]) {
|
|
// silently drop — vt10x would misinterpret
|
|
} else if data[j] == 'm' && bytes.ContainsRune(data[i+2:j], ':') {
|
|
result = append(result, 0x1b, '[')
|
|
result = append(result, rewriteSGRParams(data[i+2:j])...)
|
|
result = append(result, 'm')
|
|
} else {
|
|
result = append(result, data[start:j+1]...)
|
|
}
|
|
i = j + 1
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, data[i])
|
|
i++
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// extractCursorShape scans data for the last DECSCUSR sequence (\x1b[N SP q)
|
|
// and returns the cursor shape value (0-6). Returns -1 if none found.
|
|
// DECSCUSR: 0=default, 1=blinking block, 2=steady block, 3=blinking underline,
|
|
// 4=steady underline, 5=blinking bar, 6=steady bar.
|
|
func extractCursorShape(data []byte) int32 {
|
|
shape := int32(-1)
|
|
for i := 0; i < len(data); i++ {
|
|
if data[i] != 0x1b || i+1 >= len(data) || data[i+1] != '[' {
|
|
continue
|
|
}
|
|
j := i + 2
|
|
// Collect parameter + intermediate bytes (< 0x40)
|
|
for j < len(data) && data[j] < 0x40 {
|
|
j++
|
|
}
|
|
if j >= len(data) {
|
|
break
|
|
}
|
|
params := data[i+2 : j]
|
|
final := data[j]
|
|
// DECSCUSR: CSI Ps SP q — params end with space (0x20), final is 'q'
|
|
if final == 'q' && len(params) >= 2 && params[len(params)-1] == ' ' {
|
|
// Parse the digit(s) before the space
|
|
numPart := params[:len(params)-1]
|
|
if len(numPart) == 1 && numPart[0] >= '0' && numPart[0] <= '6' {
|
|
shape = int32(numPart[0] - '0')
|
|
}
|
|
}
|
|
i = j
|
|
}
|
|
return shape
|
|
}
|
|
|
|
// extractCursorVisible scans data for the last cursor visibility toggle
|
|
// (\x1b[?25h or \x1b[?25l). Returns 1 for show, 0 for hide, -1 if none found.
|
|
func extractCursorVisible(data []byte) int32 {
|
|
vis := int32(-1)
|
|
for i := 0; i+5 < len(data); i++ {
|
|
if data[i] != 0x1b || data[i+1] != '[' || data[i+2] != '?' ||
|
|
data[i+3] != '2' || data[i+4] != '5' {
|
|
continue
|
|
}
|
|
switch data[i+5] {
|
|
case 'h':
|
|
vis = 1
|
|
i += 5
|
|
case 'l':
|
|
vis = 0
|
|
i += 5
|
|
}
|
|
}
|
|
return vis
|
|
}
|
|
|
|
// lastCursorShowIndex returns the byte index of the last \x1b[?25h in data,
|
|
// or -1 if not found. Used to split vt10x writes so we can capture cursor
|
|
// position at the exact moment the child shows the cursor.
|
|
func lastCursorShowIndex(data []byte) int {
|
|
result := -1
|
|
for i := 0; i+5 < len(data); i++ {
|
|
if data[i] == 0x1b && data[i+1] == '[' && data[i+2] == '?' &&
|
|
data[i+3] == '2' && data[i+4] == '5' && data[i+5] == 'h' {
|
|
result = i
|
|
i += 5
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// shouldDropCSI decides whether a CSI sequence should be stripped before it
|
|
// reaches vt10x. Two categories are filtered:
|
|
//
|
|
// 1. Sequences with '>', '<', or '=' as the first parameter byte. These are
|
|
// private-use extensions (Kitty keyboard protocol, DA2 responses, etc.)
|
|
// that vt10x's CSI parser conflates with standard sequences.
|
|
//
|
|
// 2. '?'-prefixed sequences whose final byte is 'u'. The Kitty keyboard
|
|
// query (\x1b[?u) would otherwise be dispatched as DECRC (cursor restore)
|
|
// because vt10x's 'u' handler does not check the private flag.
|
|
//
|
|
// Regular '?'-prefixed sequences (\x1b[?1049h, \x1b[?25l, etc.) are NOT
|
|
// filtered — vt10x handles those correctly via its priv flag.
|
|
func shouldDropCSI(params []byte, finalByte byte) bool {
|
|
if len(params) == 0 {
|
|
return false
|
|
}
|
|
switch params[0] {
|
|
case '>', '<', '=':
|
|
return true
|
|
case '?':
|
|
return finalByte == 'u'
|
|
}
|
|
return false
|
|
}
|