1package uv
2
3import (
4 "image/color"
5 "strings"
6 "unicode"
7
8 "github.com/charmbracelet/colorprofile"
9 "github.com/charmbracelet/x/ansi"
10)
11
12// EmptyCell is a cell with a single space, width of 1, and no style or link.
13var EmptyCell = Cell{Content: " ", Width: 1}
14
15// Cell represents a single cell in the terminal screen.
16type Cell struct {
17 // Content is the [Cell]'s content, which consists of a single grapheme
18 // cluster. Most of the time, this will be a single rune as well, but it
19 // can also be a combination of runes that form a grapheme cluster.
20 Content string
21
22 // The style of the cell. Nil style means no style. Zero value prints a
23 // reset sequence.
24 Style Style
25
26 // Link is the hyperlink of the cell.
27 Link Link
28
29 // Width is the mono-spaced width of the grapheme cluster.
30 Width int
31}
32
33// NewCell creates a new cell from the given string grapheme. It will only use
34// the first grapheme in the string and ignore the rest. The width of the cell
35// is determined using the given width method.
36func NewCell(method WidthMethod, gr string) *Cell {
37 if len(gr) == 0 {
38 return &Cell{}
39 }
40 if gr == " " {
41 return EmptyCell.Clone()
42 }
43 return &Cell{
44 Content: gr,
45 Width: method.StringWidth(gr),
46 }
47}
48
49// String returns the string content of the cell excluding any styles, links,
50// and escape sequences.
51func (c *Cell) String() string {
52 return c.Content
53}
54
55// Equal returns whether the cell is equal to the other cell.
56func (c *Cell) Equal(o *Cell) bool {
57 return o != nil &&
58 c.Width == o.Width &&
59 c.Content == o.Content &&
60 c.Style.Equal(&o.Style) &&
61 c.Link.Equal(&o.Link)
62}
63
64// IsZero returns whether the cell is an empty cell.
65func (c *Cell) IsZero() bool {
66 return *c == Cell{}
67}
68
69// IsBlank returns whether the cell represents a blank cell consisting of a
70// space character.
71func (c *Cell) IsBlank() bool {
72 if c.Width <= 0 {
73 return false
74 }
75 for _, r := range c.Content {
76 if !unicode.IsSpace(r) {
77 return false
78 }
79 }
80 return c.Style.IsBlank() && c.Link.IsZero()
81}
82
83// Clone returns a copy of the cell.
84func (c *Cell) Clone() (n *Cell) {
85 n = new(Cell)
86 *n = *c
87 return
88}
89
90// Empty makes the cell an empty cell by setting its content to a single space
91// and width to 1.
92func (c *Cell) Empty() {
93 c.Content = " "
94 c.Width = 1
95}
96
97// NewLink creates a new hyperlink with the given URL and parameters.
98func NewLink(url string, params ...string) Link {
99 return Link{
100 URL: url,
101 Params: strings.Join(params, ":"),
102 }
103}
104
105// Link represents a hyperlink in the terminal screen.
106type Link struct {
107 URL string
108 Params string
109}
110
111// String returns a string representation of the hyperlink.
112func (h *Link) String() string {
113 return h.URL
114}
115
116// Equal returns whether the hyperlink is equal to the other hyperlink.
117func (h *Link) Equal(o *Link) bool {
118 return o != nil && h.URL == o.URL && h.Params == o.Params
119}
120
121// IsZero returns whether the hyperlink is empty.
122func (h *Link) IsZero() bool {
123 return *h == Link{}
124}
125
126// StyleAttrs is a bitmask for text attributes that can change the look of text.
127// These attributes can be combined to create different styles.
128type StyleAttrs uint8
129
130// These are the available text attributes that can be combined to create
131// different styles.
132const (
133 BoldAttr StyleAttrs = 1 << iota
134 FaintAttr
135 ItalicAttr
136 SlowBlinkAttr
137 RapidBlinkAttr
138 ReverseAttr
139 ConcealAttr
140 StrikethroughAttr
141
142 ResetAttr StyleAttrs = 0
143)
144
145// Add adds the attribute to the attribute mask.
146func (a StyleAttrs) Add(attr StyleAttrs) StyleAttrs {
147 return a | attr
148}
149
150// Remove removes the attribute from the attribute mask.
151func (a StyleAttrs) Remove(attr StyleAttrs) StyleAttrs {
152 return a &^ attr
153}
154
155// Contains returns whether the attribute mask contains the attribute.
156func (a StyleAttrs) Contains(attr StyleAttrs) bool {
157 return a&attr == attr
158}
159
160// UnderlineStyle is the style of underline to use for text.
161type UnderlineStyle = ansi.UnderlineStyle
162
163// These are the available underline styles.
164const (
165 NoUnderline = ansi.NoUnderlineStyle
166 SingleUnderline = ansi.SingleUnderlineStyle
167 DoubleUnderline = ansi.DoubleUnderlineStyle
168 CurlyUnderline = ansi.CurlyUnderlineStyle
169 DottedUnderline = ansi.DottedUnderlineStyle
170 DashedUnderline = ansi.DashedUnderlineStyle
171)
172
173// Style represents the Style of a cell.
174type Style struct {
175 Fg color.Color
176 Bg color.Color
177 Ul color.Color
178 UlStyle UnderlineStyle
179 Attrs StyleAttrs
180}
181
182// NewStyle is a convenience function to create a new [Style].
183func NewStyle() Style {
184 return Style{}
185}
186
187// Foreground returns a new style with the foreground color set to the given color.
188func (s Style) Foreground(c color.Color) Style {
189 s.Fg = c
190 return s
191}
192
193// Background returns a new style with the background color set to the given color.
194func (s Style) Background(c color.Color) Style {
195 s.Bg = c
196 return s
197}
198
199// Underline returns a new style with the underline color set to the given color.
200func (s Style) Underline(c color.Color) Style {
201 s.Ul = c
202 return s
203}
204
205// UnderlineStyle returns a new style with the underline style set to the
206// given style.
207func (s Style) UnderlineStyle(st UnderlineStyle) Style {
208 s.UlStyle = st
209 return s
210}
211
212// Bold returns a new style with the bold attribute set to the given value.
213func (s Style) Bold(v bool) Style {
214 if v {
215 s.Attrs = s.Attrs.Add(BoldAttr)
216 } else {
217 s.Attrs = s.Attrs.Remove(BoldAttr)
218 }
219 return s
220}
221
222// Faint returns a new style with the faint attribute set to the given value.
223func (s Style) Faint(v bool) Style {
224 if v {
225 s.Attrs = s.Attrs.Add(FaintAttr)
226 } else {
227 s.Attrs = s.Attrs.Remove(FaintAttr)
228 }
229 return s
230}
231
232// Italic returns a new style with the italic attribute set to the given value.
233func (s Style) Italic(v bool) Style {
234 if v {
235 s.Attrs = s.Attrs.Add(ItalicAttr)
236 } else {
237 s.Attrs = s.Attrs.Remove(ItalicAttr)
238 }
239 return s
240}
241
242// SlowBlink returns a new style with the slow blink attribute set to the
243// given value.
244func (s Style) SlowBlink(v bool) Style {
245 if v {
246 s.Attrs = s.Attrs.Add(SlowBlinkAttr)
247 } else {
248 s.Attrs = s.Attrs.Remove(SlowBlinkAttr)
249 }
250 return s
251}
252
253// RapidBlink returns a new style with the rapid blink attribute set to
254// the given value.
255func (s Style) RapidBlink(v bool) Style {
256 if v {
257 s.Attrs = s.Attrs.Add(RapidBlinkAttr)
258 } else {
259 s.Attrs = s.Attrs.Remove(RapidBlinkAttr)
260 }
261 return s
262}
263
264// Reverse returns a new style with the reverse attribute set to the given
265// value.
266func (s Style) Reverse(v bool) Style {
267 if v {
268 s.Attrs = s.Attrs.Add(ReverseAttr)
269 } else {
270 s.Attrs = s.Attrs.Remove(ReverseAttr)
271 }
272 return s
273}
274
275// Conceal returns a new style with the conceal attribute set to the given
276// value.
277func (s Style) Conceal(v bool) Style {
278 if v {
279 s.Attrs = s.Attrs.Add(ConcealAttr)
280 } else {
281 s.Attrs = s.Attrs.Remove(ConcealAttr)
282 }
283 return s
284}
285
286// Strikethrough returns a new style with the strikethrough attribute set to
287// the given value.
288func (s Style) Strikethrough(v bool) Style {
289 if v {
290 s.Attrs = s.Attrs.Add(StrikethroughAttr)
291 } else {
292 s.Attrs = s.Attrs.Remove(StrikethroughAttr)
293 }
294 return s
295}
296
297// Equal returns true if the style is equal to the other style.
298func (s *Style) Equal(o *Style) bool {
299 return s.Attrs == o.Attrs &&
300 s.UlStyle == o.UlStyle &&
301 colorEqual(s.Fg, o.Fg) &&
302 colorEqual(s.Bg, o.Bg) &&
303 colorEqual(s.Ul, o.Ul)
304}
305
306// Sequence returns the ANSI sequence that sets the style.
307func (s *Style) Sequence() string {
308 if s.IsZero() {
309 return ansi.ResetStyle
310 }
311
312 var b ansi.Style
313
314 if s.Attrs != 0 {
315 if s.Attrs&BoldAttr != 0 {
316 b = b.Bold()
317 }
318 if s.Attrs&FaintAttr != 0 {
319 b = b.Faint()
320 }
321 if s.Attrs&ItalicAttr != 0 {
322 b = b.Italic()
323 }
324 if s.Attrs&SlowBlinkAttr != 0 {
325 b = b.SlowBlink()
326 }
327 if s.Attrs&RapidBlinkAttr != 0 {
328 b = b.RapidBlink()
329 }
330 if s.Attrs&ReverseAttr != 0 {
331 b = b.Reverse()
332 }
333 if s.Attrs&ConcealAttr != 0 {
334 b = b.Conceal()
335 }
336 if s.Attrs&StrikethroughAttr != 0 {
337 b = b.Strikethrough()
338 }
339 }
340 if s.UlStyle != NoUnderline {
341 switch s.UlStyle {
342 case SingleUnderline:
343 b = b.Underline()
344 case DoubleUnderline:
345 b = b.DoubleUnderline()
346 case CurlyUnderline:
347 b = b.CurlyUnderline()
348 case DottedUnderline:
349 b = b.DottedUnderline()
350 case DashedUnderline:
351 b = b.DashedUnderline()
352 }
353 }
354 if s.Fg != nil {
355 b = b.ForegroundColor(s.Fg)
356 }
357 if s.Bg != nil {
358 b = b.BackgroundColor(s.Bg)
359 }
360 if s.Ul != nil {
361 b = b.UnderlineColor(s.Ul)
362 }
363
364 return b.String()
365}
366
367// DiffSequence returns the ANSI sequence that sets the style as a diff from
368// another style.
369func (s *Style) DiffSequence(o Style) string {
370 if o.IsZero() {
371 return s.Sequence()
372 }
373
374 var b ansi.Style
375
376 if !colorEqual(s.Fg, o.Fg) {
377 b = b.ForegroundColor(s.Fg)
378 }
379
380 if !colorEqual(s.Bg, o.Bg) {
381 b = b.BackgroundColor(s.Bg)
382 }
383
384 if !colorEqual(s.Ul, o.Ul) {
385 b = b.UnderlineColor(s.Ul)
386 }
387
388 var (
389 noBlink bool
390 isNormal bool
391 )
392
393 if s.Attrs != o.Attrs {
394 if s.Attrs&BoldAttr != o.Attrs&BoldAttr {
395 if s.Attrs&BoldAttr != 0 {
396 b = b.Bold()
397 } else if !isNormal {
398 isNormal = true
399 b = b.NormalIntensity()
400 }
401 }
402 if s.Attrs&FaintAttr != o.Attrs&FaintAttr {
403 if s.Attrs&FaintAttr != 0 {
404 b = b.Faint()
405 } else if !isNormal {
406 b = b.NormalIntensity()
407 }
408 }
409 if s.Attrs&ItalicAttr != o.Attrs&ItalicAttr {
410 if s.Attrs&ItalicAttr != 0 {
411 b = b.Italic()
412 } else {
413 b = b.NoItalic()
414 }
415 }
416 if s.Attrs&SlowBlinkAttr != o.Attrs&SlowBlinkAttr {
417 if s.Attrs&SlowBlinkAttr != 0 {
418 b = b.SlowBlink()
419 } else if !noBlink {
420 noBlink = true
421 b = b.NoBlink()
422 }
423 }
424 if s.Attrs&RapidBlinkAttr != o.Attrs&RapidBlinkAttr {
425 if s.Attrs&RapidBlinkAttr != 0 {
426 b = b.RapidBlink()
427 } else if !noBlink {
428 b = b.NoBlink()
429 }
430 }
431 if s.Attrs&ReverseAttr != o.Attrs&ReverseAttr {
432 if s.Attrs&ReverseAttr != 0 {
433 b = b.Reverse()
434 } else {
435 b = b.NoReverse()
436 }
437 }
438 if s.Attrs&ConcealAttr != o.Attrs&ConcealAttr {
439 if s.Attrs&ConcealAttr != 0 {
440 b = b.Conceal()
441 } else {
442 b = b.NoConceal()
443 }
444 }
445 if s.Attrs&StrikethroughAttr != o.Attrs&StrikethroughAttr {
446 if s.Attrs&StrikethroughAttr != 0 {
447 b = b.Strikethrough()
448 } else {
449 b = b.NoStrikethrough()
450 }
451 }
452 }
453
454 if s.UlStyle != o.UlStyle {
455 b = b.UnderlineStyle(s.UlStyle)
456 }
457
458 return b.String()
459}
460
461func colorEqual(c, o color.Color) bool {
462 if c == nil && o == nil {
463 return true
464 }
465 if c == nil || o == nil {
466 return false
467 }
468 cr, cg, cb, ca := c.RGBA()
469 or, og, ob, oa := o.RGBA()
470 return cr == or && cg == og && cb == ob && ca == oa
471}
472
473// IsZero returns true if the style is empty.
474func (s *Style) IsZero() bool {
475 return *s == Style{}
476}
477
478// IsBlank returns whether the style consists of only attributes that don't
479// affect appearance of a space character.
480func (s *Style) IsBlank() bool {
481 return s.UlStyle == NoUnderline &&
482 s.Attrs&^(BoldAttr|FaintAttr|ItalicAttr|SlowBlinkAttr|RapidBlinkAttr) == 0 &&
483 s.Fg == nil &&
484 s.Bg == nil &&
485 s.Ul == nil
486}
487
488// Convert converts a style to respect the given color profile.
489func ConvertStyle(s Style, p colorprofile.Profile) Style {
490 switch p {
491 case colorprofile.TrueColor:
492 return s
493 case colorprofile.Ascii:
494 s.Fg = nil
495 s.Bg = nil
496 s.Ul = nil
497 case colorprofile.NoTTY:
498 return Style{}
499 }
500
501 if s.Fg != nil {
502 s.Fg = p.Convert(s.Fg)
503 }
504 if s.Bg != nil {
505 s.Bg = p.Convert(s.Bg)
506 }
507 if s.Ul != nil {
508 s.Ul = p.Convert(s.Ul)
509 }
510 return s
511}
512
513// Convert converts a hyperlink to respect the given color profile.
514func ConvertLink(h Link, p colorprofile.Profile) Link {
515 if p == colorprofile.NoTTY {
516 return Link{}
517 }
518 return h
519}