src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/ui/text.go (about) 1 package ui 2 3 import ( 4 "bytes" 5 "fmt" 6 "math/big" 7 "strconv" 8 "strings" 9 10 "src.elv.sh/pkg/eval/vals" 11 "src.elv.sh/pkg/wcwidth" 12 ) 13 14 // Text contains of a list of styled Segments. 15 // 16 // When only functions in this package are used to manipulate Text instances, 17 // they will always satisfy the following properties: 18 // 19 // - If the Text is empty, it is nil (not a non-nil slice of size 0). 20 // 21 // - No [Segment] in it has an empty Text field. 22 // 23 // - No two adjacent [Segment] instances have the same [Style]. 24 type Text []*Segment 25 26 // T constructs a new Text with the given content and the given Styling's 27 // applied. 28 func T(s string, ts ...Styling) Text { 29 if s == "" { 30 return nil 31 } 32 return StyleText(Text{&Segment{Text: s}}, ts...) 33 } 34 35 // Concat concatenates multiple Text's into one. 36 func Concat(texts ...Text) Text { 37 var tb TextBuilder 38 for _, text := range texts { 39 tb.WriteText(text) 40 } 41 return tb.Text() 42 } 43 44 // Kind returns "styled-text". 45 func (Text) Kind() string { return "ui:text" } 46 47 // Repr returns the representation of the current Text. It is just a wrapper 48 // around the containing Segments. 49 func (t Text) Repr(indent int) string { 50 buf := new(bytes.Buffer) 51 for _, s := range t { 52 buf.WriteByte(' ') 53 buf.WriteString(s.Repr(indent + 1)) 54 } 55 return fmt.Sprintf("[^styled%s]", buf.String()) 56 } 57 58 // IterateKeys feeds the function with all valid indices of the styled-text. 59 func (t Text) IterateKeys(fn func(any) bool) { 60 for i := 0; i < len(t); i++ { 61 if !fn(strconv.Itoa(i)) { 62 break 63 } 64 } 65 } 66 67 // Index provides access to the underlying styled-segment. 68 func (t Text) Index(k any) (any, error) { 69 index, err := vals.ConvertListIndex(k, len(t)) 70 if err != nil { 71 return nil, err 72 } else if index.Slice { 73 return t[index.Lower:index.Upper], nil 74 } else { 75 return t[index.Lower], nil 76 } 77 } 78 79 // Concat implements Text+string, Text+number, Text+Segment and Text+Text. 80 func (t Text) Concat(rhs any) (any, error) { 81 switch rhs := rhs.(type) { 82 case string: 83 return Concat(t, T(rhs)), nil 84 case int, *big.Int, *big.Rat, float64: 85 return Concat(t, T(vals.ToString(rhs))), nil 86 case *Segment: 87 return Concat(t, Text{rhs}), nil 88 case Text: 89 return Concat(t, rhs), nil 90 } 91 92 return nil, vals.ErrConcatNotImplemented 93 } 94 95 // RConcat implements string+Text and number+Text. 96 func (t Text) RConcat(lhs any) (any, error) { 97 switch lhs := lhs.(type) { 98 case string: 99 return Concat(T(lhs), t), nil 100 case int, *big.Int, *big.Rat, float64: 101 return Concat(T(vals.ToString(lhs)), t), nil 102 } 103 104 return nil, vals.ErrConcatNotImplemented 105 } 106 107 // Partition partitions the Text at n indices into n+1 Text values. 108 func (t Text) Partition(indices ...int) []Text { 109 out := make([]Text, len(indices)+1) 110 segs := t.Clone() 111 for i, idx := range indices { 112 toConsume := idx 113 if i > 0 { 114 toConsume -= indices[i-1] 115 } 116 for len(segs) > 0 && toConsume > 0 { 117 if len(segs[0].Text) <= toConsume { 118 out[i] = append(out[i], segs[0]) 119 toConsume -= len(segs[0].Text) 120 segs = segs[1:] 121 } else { 122 out[i] = append(out[i], &Segment{segs[0].Style, segs[0].Text[:toConsume]}) 123 segs[0] = &Segment{segs[0].Style, segs[0].Text[toConsume:]} 124 toConsume = 0 125 } 126 } 127 } 128 if len(segs) > 0 { 129 // Don't use segs directly to avoid memory leak 130 out[len(indices)] = append(Text(nil), segs...) 131 } 132 return out 133 } 134 135 // Clone returns a deep copy of Text. 136 func (t Text) Clone() Text { 137 newt := make(Text, len(t)) 138 for i, seg := range t { 139 newt[i] = seg.Clone() 140 } 141 return newt 142 } 143 144 // CountRune counts the number of times a rune occurs in a Text. 145 func (t Text) CountRune(r rune) int { 146 n := 0 147 for _, seg := range t { 148 n += seg.CountRune(r) 149 } 150 return n 151 } 152 153 // CountLines counts the number of lines in a Text. It is equal to 154 // t.CountRune('\n') + 1. 155 func (t Text) CountLines() int { 156 return t.CountRune('\n') + 1 157 } 158 159 // SplitByRune splits a Text by the given rune. 160 func (t Text) SplitByRune(r rune) []Text { 161 if len(t) == 0 { 162 return nil 163 } 164 // Call SplitByRune for each constituent Segment, and "paste" the pairs of 165 // subsegments across the segment border. For instance, if Text has 3 166 // Segments a, b, c that results in a1, a2, a3, b1, b2, c1, then a3 and b1 167 // as well as b2 and c1 are pasted together, and the return value is [a1], 168 // [a2], [a3, b1], [b2, c1]. Pasting can coalesce: for instance, if 169 // Text has 3 Segments a, b, c that results in a1, a2, b1, c1, the return 170 // value will be [a1], [a2, b1, c1]. 171 var result []Text 172 var paste TextBuilder 173 for _, seg := range t { 174 subSegs := seg.SplitByRune(r) 175 // Paste the first segment. 176 paste.WriteText(TextFromSegment(subSegs[0])) 177 if len(subSegs) == 1 { 178 // Only one subsegment. Keep the paste active. 179 continue 180 } 181 // Add the paste and reset it. 182 result = append(result, paste.Text()) 183 paste.Reset() 184 // For the subsegments in the middle, just add then as is. 185 for i := 1; i < len(subSegs)-1; i++ { 186 result = append(result, TextFromSegment(subSegs[i])) 187 } 188 // The last segment becomes the new paste. 189 paste.WriteText(TextFromSegment(subSegs[len(subSegs)-1])) 190 } 191 result = append(result, paste.Text()) 192 return result 193 } 194 195 // TrimWcwidth returns the largest prefix of t that does not exceed the given 196 // visual width. 197 func (t Text) TrimWcwidth(wmax int) Text { 198 var newt Text 199 for _, seg := range t { 200 w := wcwidth.Of(seg.Text) 201 if w >= wmax { 202 newt = append(newt, 203 &Segment{seg.Style, wcwidth.Trim(seg.Text, wmax)}) 204 break 205 } 206 wmax -= w 207 newt = append(newt, seg) 208 } 209 return newt 210 } 211 212 // String returns a string representation of the styled text. This now always 213 // assumes VT-style terminal output. 214 // 215 // TODO: Make string conversion sensible to environment, e.g. use HTML when 216 // output is web. 217 func (t Text) String() string { 218 return t.VTString() 219 } 220 221 // VTString renders the styled text using VT-style escape sequences. Any 222 // existing SGR state will be cleared. 223 func (t Text) VTString() string { 224 var sb strings.Builder 225 clean := false 226 for _, seg := range t { 227 sgr := seg.SGR() 228 if sgr == "" { 229 if !clean { 230 sb.WriteString("\033[m") 231 } 232 clean = true 233 } else { 234 if clean { 235 sb.WriteString("\033[" + sgr + "m") 236 } else { 237 sb.WriteString("\033[;" + sgr + "m") 238 } 239 clean = false 240 } 241 sb.WriteString(seg.Text) 242 } 243 if !clean { 244 sb.WriteString("\033[m") 245 } 246 return sb.String() 247 } 248 249 // TextFromSegment returns a [Text] with just seg if seg.Text is non-empty. 250 // Otherwise it returns nil. 251 func TextFromSegment(seg *Segment) Text { 252 if seg.Text == "" { 253 return nil 254 } 255 return Text{seg} 256 }