package ui import ( "bytes" "fmt" "math/big" "strings" "src.elv.sh/pkg/eval/vals" ) // Segment is a string that has some style applied to it. type Segment struct { Style Text string } // Kind returns "styled-segment". func (*Segment) Kind() string { return "ui:text-segment" } // Repr returns the representation of this Segment. The string can be used to // construct an identical Segment. Unset or default attributes are skipped. If // the Segment represents an unstyled string only this string is returned. func (s *Segment) Repr(int) string { buf := new(bytes.Buffer) addIfNotEqual := func(key string, val, cmp any) { if val != cmp { var valString string if c, ok := val.(Color); ok { valString = c.String() } else { valString = vals.Repr(val, 0) } fmt.Fprintf(buf, "&%s=%s ", key, valString) } } addIfNotEqual("fg-color", s.Foreground, nil) addIfNotEqual("bg-color", s.Background, nil) addIfNotEqual("bold", s.Bold, false) addIfNotEqual("dim", s.Dim, false) addIfNotEqual("italic", s.Italic, false) addIfNotEqual("underlined", s.Underlined, false) addIfNotEqual("blink", s.Blink, false) addIfNotEqual("inverse", s.Inverse, false) if buf.Len() == 0 { return s.Text } return fmt.Sprintf("(ui:text-segment %s %s)", s.Text, strings.TrimSpace(buf.String())) } // IterateKeys feeds the function with all valid attributes of styled-segment. func (*Segment) IterateKeys(fn func(v any) bool) { vals.Feed(fn, "text", "fg-color", "bg-color", "bold", "dim", "italic", "underlined", "blink", "inverse") } // Index provides access to the attributes of a styled-segment. func (s *Segment) Index(k any) (v any, ok bool) { switch k { case "text": v = s.Text case "fg-color": if s.Foreground == nil { return "default", true } return s.Foreground.String(), true case "bg-color": if s.Background == nil { return "default", true } return s.Background.String(), true case "bold": v = s.Bold case "dim": v = s.Dim case "italic": v = s.Italic case "underlined": v = s.Underlined case "blink": v = s.Blink case "inverse": v = s.Inverse } return v, v != nil } // Concat implements Segment+string, Segment+float64, Segment+Segment and // Segment+Text. func (s *Segment) Concat(v any) (any, error) { switch rhs := v.(type) { case string: return Text{s, &Segment{Text: rhs}}, nil case *Segment: return Text{s, rhs}, nil case Text: return Text(append([]*Segment{s}, rhs...)), nil case int, *big.Int, *big.Rat, float64: return Text{s, &Segment{Text: vals.ToString(rhs)}}, nil } return nil, vals.ErrConcatNotImplemented } // RConcat implements string+Segment and float64+Segment. func (s *Segment) RConcat(v any) (any, error) { switch lhs := v.(type) { case string: return Text{&Segment{Text: lhs}, s}, nil case int, *big.Int, *big.Rat, float64: return Text{&Segment{Text: vals.ToString(lhs)}, s}, nil } return nil, vals.ErrConcatNotImplemented } // Clone returns a copy of the Segment. func (s *Segment) Clone() *Segment { value := *s return &value } // CountRune counts the number of times a rune occurs in a Segment. func (s *Segment) CountRune(r rune) int { return strings.Count(s.Text, string(r)) } // SplitByRune splits a Segment by the given rune. func (s *Segment) SplitByRune(r rune) []*Segment { splitTexts := strings.Split(s.Text, string(r)) splitSegs := make([]*Segment, len(splitTexts)) for i, splitText := range splitTexts { splitSegs[i] = &Segment{s.Style, splitText} } return splitSegs } // String returns a string representation of the styled segment. This now always // assumes VT-style terminal output. // TODO: Make string conversion sensible to environment, e.g. use HTML when // output is web. func (s *Segment) String() string { return s.VTString() } // VTString renders the styled segment using VT-style escape sequences. Any // existing SGR state will be cleared. func (s *Segment) VTString() string { sgr := s.SGR() if sgr == "" { return "\033[m" + s.Text } return fmt.Sprintf("\033[;%sm%s\033[m", sgr, s.Text) }