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 }