crt/csi.go
2023-05-23 20:53:49 +02:00

226 lines
4.2 KiB
Go

package crt
import (
"github.com/muesli/termenv"
"strconv"
"strings"
"sync"
)
var csiMtx = &sync.Mutex{}
var csiCache = map[string]any{}
type CursorUpSeq struct {
Count int
}
type CursorDownSeq struct {
Count int
}
type CursorForwardSeq struct {
Count int
}
type CursorBackSeq struct {
Count int
}
type CursorNextLineSeq struct {
Count int
}
type CursorPreviousLineSeq struct {
Count int
}
type CursorHorizontalSeq struct {
Count int
}
type CursorPositionSeq struct {
Row int
Col int
}
type EraseDisplaySeq struct {
Type int
}
type EraseLineSeq struct {
Type int
}
type ScrollUpSeq struct {
Count int
}
type ScrollDownSeq struct {
Count int
}
type SaveCursorPositionSeq struct{}
type RestoreCursorPositionSeq struct{}
type ChangeScrollingRegionSeq struct {
Top int
Bottom int
}
type InsertLineSeq struct {
Count int
}
type DeleteLineSeq struct {
Count int
}
type CursorShowSeq struct{}
type CursorHideSeq struct{}
// extractCSI extracts a CSI sequence from the beginning of a string.
// It returns the sequence without any suffix, and a boolean indicating
// whether a sequence was found.
func extractCSI(s string) (string, bool) {
if !strings.HasPrefix(s, termenv.CSI) {
return "", false
}
s = s[len(termenv.CSI):]
if len(s) == 0 {
return "", false
}
for i, c := range s {
if c >= '@' && c <= '~' {
return termenv.CSI + s[:i+1], true
}
}
return "", false
}
// parseCSI parses a CSI sequence and returns a struct representing the sequence.
func parseCSI(s string) (any, bool) {
if !strings.HasPrefix(s, termenv.CSI) {
return nil, false
}
s = s[len(termenv.CSI):]
if len(s) == 0 {
return nil, false
}
csiMtx.Lock()
if cached, ok := csiCache[s]; ok {
csiMtx.Unlock()
return cached, true
}
csiMtx.Unlock()
if val, ok := parseCSIStruct(s); ok {
csiMtx.Lock()
csiCache[s] = val
csiMtx.Unlock()
return val, true
}
return nil, false
}
func parseCSIStruct(s string) (any, bool) {
switch s {
case termenv.ShowCursorSeq:
return CursorShowSeq{}, true
case termenv.HideCursorSeq:
return CursorHideSeq{}, true
}
switch s[len(s)-1] {
case 'A':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorUpSeq{Count: count}, true
}
case 'B':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorDownSeq{Count: count}, true
}
case 'C':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorForwardSeq{Count: count}, true
}
case 'D':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorBackSeq{Count: count}, true
}
case 'E':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorNextLineSeq{Count: count}, true
}
case 'F':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorPreviousLineSeq{Count: count}, true
}
case 'G':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return CursorHorizontalSeq{Count: count}, true
}
case 'H':
if strings.Contains(s, ";") {
parts := strings.Split(s[:len(s)-1], ";")
if len(parts) != 2 {
return nil, false
}
row, err := strconv.Atoi(parts[0])
if err != nil {
return nil, false
}
col, err := strconv.Atoi(parts[1])
if err != nil {
return nil, false
}
return CursorPositionSeq{Row: row, Col: col}, true
}
return nil, false
case 'J':
if t, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return EraseDisplaySeq{Type: t}, true
}
case 'K':
if t, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return EraseLineSeq{Type: t}, true
}
case 'S':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return ScrollUpSeq{Count: count}, true
}
case 'T':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return ScrollDownSeq{Count: count}, true
}
case 's':
if len(s) == 1 {
return SaveCursorPositionSeq{}, true
}
case 'u':
if len(s) == 1 {
return RestoreCursorPositionSeq{}, true
}
case 'r':
// TODO: implement
case 'L':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return InsertLineSeq{Count: count}, true
}
case 'M':
if count, err := strconv.Atoi(s[:len(s)-1]); err == nil {
return DeleteLineSeq{Count: count}, true
}
}
return nil, false
}