cell.go

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