cell.go

  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}