1package uv
2
3import (
4 "bytes"
5 "image/color"
6 "strings"
7
8 "github.com/charmbracelet/x/ansi"
9)
10
11// StyledString is a string that can be decomposed into a series of styled
12// lines and cells. It is used to disassemble a rendered string with ANSI
13// escape codes into a series of cells that can be used in a [Buffer].
14// A StyledString supports reading [ansi.SGR] and [ansi.Hyperlink] escape
15// codes.
16type StyledString struct {
17 // Text is the original string that was used to create the styled string.
18 Text string
19 // Wrap determines whether the styled string should wrap to the next line.
20 Wrap bool
21 // Tail is the string that will be appended to the end of the line when the
22 // string is truncated i.e. when [StyledString.Wrap] is false.
23 Tail string
24}
25
26var _ Drawable = (*StyledString)(nil)
27
28// NewStyledString creates a new [StyledString] for the given method and styled
29// string. The method is used to calculate the width of each line.
30func NewStyledString(str string) *StyledString {
31 ss := new(StyledString)
32 ss.Text = str
33 return ss
34}
35
36// Draw renders the styled string to the given buffer at the
37// specified area.
38func (s *StyledString) Draw(buf Screen, area Rectangle) {
39 // Clear the area before drawing.
40 for y := area.Min.Y; y < area.Max.Y; y++ {
41 for x := area.Min.X; x < area.Max.X; x++ {
42 buf.SetCell(x, y, nil)
43 }
44 }
45 str := s.Text
46 // We need to normalize newlines "\n" to "\r\n" to emulate a raw terminal
47 // output.
48 str = strings.ReplaceAll(str, "\r\n", "\n")
49 str = strings.ReplaceAll(str, "\n", "\r\n")
50 printString(buf, ansi.GraphemeWidth, area.Min.X, area.Min.Y, area, str, !s.Wrap, s.Tail)
51}
52
53// Height returns the number of lines in the styled string. This is the number
54// of lines that the styled string will occupy when rendered to the screen.
55func (s *StyledString) Height() int {
56 return strings.Count(s.Text, "\n") + 1
57}
58
59// UnicodeWidth returns the cells width of the widest line in the styled string
60// using the [ansi.GraphemeWidth] method.
61func (s *StyledString) UnicodeWidth() int {
62 w, _ := s.widthHeight(ansi.GraphemeWidth)
63 return w
64}
65
66// WcWidth returns the cells width of the widest line in the styled string
67// using the [ansi.WcWidth] method.
68func (s *StyledString) WcWidth() int {
69 w, _ := s.widthHeight(ansi.WcWidth)
70 return w
71}
72
73func (s *StyledString) widthHeight(m ansi.Method) (w, h int) {
74 lines := strings.Split(s.Text, "\n")
75 h = len(lines)
76 for _, l := range lines {
77 w = max(w, m.StringWidth(l))
78 }
79 return
80}
81
82// Bounds returns the minimum area that can contain the whole styled string.
83func (s *StyledString) Bounds() Rectangle {
84 w, h := s.widthHeight(ansi.GraphemeWidth)
85 return Rect(0, 0, w, h)
86}
87
88// printString draws a string starting at the given position.
89func printString[T []byte | string](
90 s Screen,
91 m ansi.Method,
92 x, y int,
93 bounds Rectangle, str T,
94 truncate bool, tail string,
95) {
96 p := ansi.NewParser()
97
98 var tailc Cell
99 if truncate && len(tail) > 0 {
100 tailc = *NewCell(m, tail)
101 }
102
103 decoder := ansi.DecodeSequenceWc[T]
104 if m == ansi.GraphemeWidth {
105 decoder = ansi.DecodeSequence[T]
106 }
107
108 var cell Cell
109 var style Style
110 var link Link
111 var state byte
112 for len(str) > 0 {
113 seq, width, n, newState := decoder(str, state, p)
114 switch width {
115 case 1, 2, 3, 4: // wide cells can go up to 4 cells wide
116 cell.Width = width
117 cell.Content = string(seq)
118
119 if !truncate && x+cell.Width > bounds.Max.X && y+1 < bounds.Max.Y {
120 // Wrap the string to the width of the window
121 x = bounds.Min.X
122 y++
123 }
124
125 pos := Pos(x, y)
126 if pos.In(bounds) {
127 if truncate && tailc.Width > 0 && x+cell.Width > bounds.Max.X-tailc.Width {
128 // Truncate the string and append the tail if any.
129 cell = tailc
130 cell.Style = style
131 cell.Link = link
132 s.SetCell(x, y, &cell)
133 x += tailc.Width
134 } else {
135 // Print the cell to the screen
136 cell.Style = style
137 cell.Link = link
138 s.SetCell(x, y, &cell) //nolint:errcheck
139 x += width
140 }
141
142 // String is too long for the line, truncate it.
143 // Make sure we reset the cell for the next iteration.
144 cell = Cell{}
145 }
146 default:
147 // Valid sequences always have a non-zero Cmd.
148 // TODO: Handle cursor movement and other sequences
149 switch {
150 case ansi.HasCsiPrefix(seq) && p.Command() == 'm':
151 // SGR - Select Graphic Rendition
152 ReadStyle(p.Params(), &style)
153 case ansi.HasOscPrefix(seq) && p.Command() == 8:
154 // Hyperlinks
155 ReadLink(p.Data(), &link)
156 case ansi.Equal(seq, T("\n")):
157 y++
158 case ansi.Equal(seq, T("\r")):
159 x = bounds.Min.X
160 default:
161 cell.Content += string(seq)
162 }
163 }
164
165 // Advance the state and data
166 state = newState
167 str = str[n:]
168 }
169
170 // Make sure to set the last cell if it's not empty.
171 if !cell.IsZero() {
172 s.SetCell(x, y, &cell) //nolint:errcheck
173 cell = Cell{}
174 }
175}
176
177// ReadStyle reads a Select Graphic Rendition (SGR) escape sequences from a
178// list of parameters into pen.
179func ReadStyle(params ansi.Params, pen *Style) {
180 if len(params) == 0 {
181 *pen = Style{}
182 return
183 }
184
185 for i := 0; i < len(params); i++ {
186 param, hasMore, _ := params.Param(i, 0)
187 switch param {
188 case 0: // Reset
189 *pen = Style{}
190 case 1: // Bold
191 *pen = pen.Bold(true)
192 case 2: // Dim/Faint
193 *pen = pen.Faint(true)
194 case 3: // Italic
195 *pen = pen.Italic(true)
196 case 4: // Underline
197 nextParam, _, ok := params.Param(i+1, 0)
198 if hasMore && ok { // Only accept subparameters i.e. separated by ":"
199 switch nextParam {
200 case 0, 1, 2, 3, 4, 5:
201 i++
202 switch nextParam {
203 case 0: // No Underline
204 *pen = pen.UnderlineStyle(NoUnderline)
205 case 1: // Single Underline
206 *pen = pen.UnderlineStyle(SingleUnderline)
207 case 2: // Double Underline
208 *pen = pen.UnderlineStyle(DoubleUnderline)
209 case 3: // Curly Underline
210 *pen = pen.UnderlineStyle(CurlyUnderline)
211 case 4: // Dotted Underline
212 *pen = pen.UnderlineStyle(DottedUnderline)
213 case 5: // Dashed Underline
214 *pen = pen.UnderlineStyle(DashedUnderline)
215 }
216 }
217 } else {
218 // Single Underline
219 *pen = pen.UnderlineStyle(SingleUnderline)
220 }
221 case 5: // Slow Blink
222 *pen = pen.SlowBlink(true)
223 case 6: // Rapid Blink
224 *pen = pen.RapidBlink(true)
225 case 7: // Reverse
226 *pen = pen.Reverse(true)
227 case 8: // Conceal
228 *pen = pen.Conceal(true)
229 case 9: // Crossed-out/Strikethrough
230 *pen = pen.Strikethrough(true)
231 case 22: // Normal Intensity (not bold or faint)
232 *pen = pen.Bold(false).Faint(false)
233 case 23: // Not italic, not Fraktur
234 *pen = pen.Italic(false)
235 case 24: // Not underlined
236 *pen = pen.UnderlineStyle(NoUnderline)
237 case 25: // Blink off
238 *pen = pen.SlowBlink(false).RapidBlink(false)
239 case 27: // Positive (not reverse)
240 *pen = pen.Reverse(false)
241 case 28: // Reveal
242 *pen = pen.Conceal(false)
243 case 29: // Not crossed out
244 *pen = pen.Strikethrough(false)
245 case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground
246 *pen = pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec
247 case 38: // Set foreground 256 or truecolor
248 var c color.Color
249 n := ansi.ReadStyleColor(params[i:], &c)
250 if n > 0 {
251 *pen = pen.Foreground(c)
252 i += n - 1
253 }
254 case 39: // Default foreground
255 *pen = pen.Foreground(nil)
256 case 40, 41, 42, 43, 44, 45, 46, 47: // Set background
257 *pen = pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec
258 case 48: // Set background 256 or truecolor
259 var c color.Color
260 n := ansi.ReadStyleColor(params[i:], &c)
261 if n > 0 {
262 *pen = pen.Background(c)
263 i += n - 1
264 }
265 case 49: // Default Background
266 *pen = pen.Background(nil)
267 case 58: // Set underline color
268 var c color.Color
269 n := ansi.ReadStyleColor(params[i:], &c)
270 if n > 0 {
271 *pen = pen.Underline(c)
272 i += n - 1
273 }
274 case 59: // Default underline color
275 *pen = pen.Underline(nil)
276 case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground
277 *pen = pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec
278 case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background
279 *pen = pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec
280 }
281 }
282}
283
284// ReadLink reads a hyperlink escape sequence from a data buffer into link.
285func ReadLink(p []byte, link *Link) {
286 params := bytes.Split(p, []byte{';'})
287 if len(params) != 3 {
288 return
289 }
290 link.Params = string(params[1])
291 link.URL = string(params[2])
292}