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