okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Christian Rochafix(renderer): make wide-cell placeholder handling consistentc7cac3e

upstream
11.2 KiB465 linesraw
1package uv
2
3import (
4	"image/color"
5	"strings"
6
7	"github.com/charmbracelet/colorprofile"
8	"github.com/charmbracelet/x/ansi"
9)
10
11// EmptyCell is a cell with a single space, width of 1, and no style or link.
12var EmptyCell = Cell{Content: " ", Width: 1}
13
14// Cell represents a single cell in the terminal screen.
15type Cell struct {
16	// Content is the [Cell]'s content, which consists of a single grapheme
17	// cluster. Most of the time, this will be a single rune as well, but it
18	// can also be a combination of runes that form a grapheme cluster.
19	Content string
20
21	// The style of the cell. Nil style means no style. Zero value prints a
22	// reset sequence.
23	Style Style
24
25	// Link is the hyperlink of the cell.
26	Link Link
27
28	// Width is the mono-spaced width of the grapheme cluster.
29	Width int
30}
31
32// NewCell creates a new cell from the given string grapheme. It will only use
33// the first grapheme in the string and ignore the rest. The width of the cell
34// is determined using the given width method.
35func NewCell(method WidthMethod, gr string) *Cell {
36	if len(gr) == 0 {
37		return &Cell{}
38	}
39	if gr == " " {
40		return EmptyCell.Clone()
41	}
42	return &Cell{
43		Content: gr,
44		Width:   method.StringWidth(gr),
45	}
46}
47
48// String returns the string content of the cell excluding any styles, links,
49// and escape sequences.
50func (c *Cell) String() string {
51	return c.Content
52}
53
54// Equal returns whether the cell is equal to the other cell.
55func (c *Cell) Equal(o *Cell) bool {
56	return o != nil &&
57		c.Width == o.Width &&
58		c.Content == o.Content &&
59		c.Style.Equal(&o.Style) &&
60		c.Link.Equal(&o.Link)
61}
62
63// IsZero returns whether the cell is an empty cell.
64func (c *Cell) IsZero() bool {
65	return *c == Cell{}
66}
67
68// isWidePlaceholder reports whether the cell is the continuation column of a
69// wide cell, marked with a zero display width.
70func (c *Cell) isWidePlaceholder() bool {
71	return c != nil && c.Width == 0
72}
73
74// Clone returns a copy of the cell.
75func (c *Cell) Clone() (n *Cell) {
76	n = new(Cell)
77	*n = *c
78	return
79}
80
81// Empty makes the cell an empty cell by setting its content to a single space
82// and width to 1.
83func (c *Cell) Empty() {
84	c.Content = " "
85	c.Width = 1
86}
87
88// NewLink creates a new hyperlink with the given URL and parameters.
89func NewLink(url string, params ...string) Link {
90	return Link{
91		URL:    url,
92		Params: strings.Join(params, ":"),
93	}
94}
95
96// Link represents a hyperlink in the terminal screen.
97type Link struct {
98	URL    string
99	Params string
100}
101
102// String returns a string representation of the hyperlink.
103func (h *Link) String() string {
104	return h.URL
105}
106
107// Equal returns whether the hyperlink is equal to the other hyperlink.
108func (h *Link) Equal(o *Link) bool {
109	return o != nil && *h == *o
110}
111
112// IsZero returns whether the hyperlink is empty.
113func (h *Link) IsZero() bool {
114	return *h == Link{}
115}
116
117// These are the available text attributes that can be combined to create
118// different styles.
119const (
120	AttrBold = 1 << iota
121	AttrFaint
122	AttrItalic
123	AttrBlink
124	AttrRapidBlink // Not widely supported
125	AttrReverse
126	AttrConceal
127	AttrStrikethrough
128
129	AttrReset = 0
130)
131
132// AttrSlowBlink is an alias for AttrBlink.
133//
134// Deprecated: Use [AttrBlink] instead.
135const (
136	AttrSlowBlink = AttrBlink
137)
138
139// Underline is the style of underline to use for text.
140type Underline = ansi.Underline
141
142// UnderlineStyle is the style of underline to use for text.
143//
144// Deprecated: Use [Underline] instead.
145type UnderlineStyle = ansi.Underline
146
147// These are the available underline styles.
148const (
149	UnderlineNone   = ansi.UnderlineNone
150	UnderlineSingle = ansi.UnderlineSingle
151	UnderlineDouble = ansi.UnderlineDouble
152	UnderlineCurly  = ansi.UnderlineCurly
153	UnderlineDotted = ansi.UnderlineDotted
154	UnderlineDashed = ansi.UnderlineDashed
155)
156
157// These are the available underline styles.
158//
159// Deprecated: Use the constants from [Underline] instead.
160const (
161	UnderlineStyleNone   = ansi.UnderlineNone
162	UnderlineStyleSingle = ansi.UnderlineSingle
163	UnderlineStyleDouble = ansi.UnderlineDouble
164	UnderlineStyleCurly  = ansi.UnderlineCurly
165	UnderlineStyleDotted = ansi.UnderlineDotted
166	UnderlineStyleDashed = ansi.UnderlineDashed
167)
168
169// Style represents the style of a cell.
170type Style struct {
171	Fg             color.Color
172	Bg             color.Color
173	UnderlineColor color.Color
174	Underline      Underline
175	Attrs          uint8
176}
177
178// Equal returns true if the style is equal to the other style.
179func (s *Style) Equal(o *Style) bool {
180	return s.Attrs == o.Attrs &&
181		s.Underline == o.Underline &&
182		colorEqual(s.Fg, o.Fg) &&
183		colorEqual(s.Bg, o.Bg) &&
184		colorEqual(s.UnderlineColor, o.UnderlineColor)
185}
186
187// Styled wraps the given string with the style's ANSI sequences and resets.
188func (s *Style) Styled(str string) string {
189	if s.IsZero() {
190		return str
191	}
192	return s.String() + str + ansi.ResetStyle
193}
194
195// String returns the ANSI SGR sequence for the style.
196func (s *Style) String() string {
197	if s.IsZero() {
198		return ansi.ResetStyle
199	}
200
201	var b ansi.Style
202
203	if s.Attrs != 0 { //nolint:nestif
204		if s.Attrs&AttrBold != 0 {
205			b = b.Bold()
206		}
207		if s.Attrs&AttrFaint != 0 {
208			b = b.Faint()
209		}
210		if s.Attrs&AttrItalic != 0 {
211			b = b.Italic(true)
212		}
213		if s.Attrs&AttrBlink != 0 {
214			b = b.Blink(true)
215		}
216		if s.Attrs&AttrRapidBlink != 0 {
217			b = b.RapidBlink(true)
218		}
219		if s.Attrs&AttrReverse != 0 {
220			b = b.Reverse(true)
221		}
222		if s.Attrs&AttrConceal != 0 {
223			b = b.Conceal(true)
224		}
225		if s.Attrs&AttrStrikethrough != 0 {
226			b = b.Strikethrough(true)
227		}
228	}
229	if s.Underline != UnderlineStyleNone {
230		switch s.Underline {
231		case UnderlineStyleSingle:
232			b = b.Underline(true)
233		case UnderlineStyleDouble:
234			b = b.UnderlineStyle(UnderlineStyleDouble)
235		case UnderlineStyleCurly:
236			b = b.UnderlineStyle(UnderlineStyleCurly)
237		case UnderlineStyleDotted:
238			b = b.UnderlineStyle(UnderlineStyleDotted)
239		case UnderlineStyleDashed:
240			b = b.UnderlineStyle(UnderlineStyleDashed)
241		}
242	}
243	if s.Fg != nil {
244		b = b.ForegroundColor(s.Fg)
245	}
246	if s.Bg != nil {
247		b = b.BackgroundColor(s.Bg)
248	}
249	if s.UnderlineColor != nil {
250		b = b.UnderlineColor(s.UnderlineColor)
251	}
252
253	return b.String()
254}
255
256// Diff returns the ANSI sequence that sets the style as a diff from
257// another style.
258func (s *Style) Diff(from *Style) string {
259	return StyleDiff(from, s)
260}
261
262// StyleDiff returns the SGR ANSI sequence necessary to transition from the
263// "from" style to the "to" style.
264func StyleDiff(from, to *Style) string {
265	if from == nil && to == nil {
266		return ""
267	}
268	if from != nil && to != nil && from.Equal(to) {
269		return ""
270	}
271	if from == nil {
272		return to.String()
273	}
274	if to == nil || to.IsZero() {
275		// Resetting all styles is cheaper than calculating diffs.
276		// "\x1b[m" is 3 bytes vs potentially much longer sequences.
277		return ansi.ResetStyle
278	}
279
280	// TODO: Optimize further by checking if a full reset is cheaper than
281	// calculating diffs. Often, it might be cheaper to reset everything and
282	// then set the desired styles rather than calculating diffs. Also more
283	// compatible with terminals that have buggy SGR implementations.
284
285	var b ansi.Style
286
287	if !colorEqual(from.Fg, to.Fg) {
288		b = b.ForegroundColor(to.Fg)
289	}
290
291	if !colorEqual(from.Bg, to.Bg) {
292		b = b.BackgroundColor(to.Bg)
293	}
294
295	if !colorEqual(from.UnderlineColor, to.UnderlineColor) {
296		// TODO: Investigate this. For backward compatibility, we might want to
297		// set this at the end instead. Because on terminals that don't support
298		// underline color, this might mess up some other attributes if set in
299		// the middle.
300		b = b.UnderlineColor(to.UnderlineColor)
301	}
302
303	fromBold := from.Attrs&AttrBold != 0
304	fromFaint := from.Attrs&AttrFaint != 0
305	fromItalic := from.Attrs&AttrItalic != 0
306	fromUnderline := from.Underline != UnderlineStyleNone
307	fromBlink := from.Attrs&AttrBlink != 0
308	fromRapidBlink := from.Attrs&AttrRapidBlink != 0
309	fromReverse := from.Attrs&AttrReverse != 0
310	fromConceal := from.Attrs&AttrConceal != 0
311	fromStrikethrough := from.Attrs&AttrStrikethrough != 0
312	toBold := to.Attrs&AttrBold != 0
313	toFaint := to.Attrs&AttrFaint != 0
314	toItalic := to.Attrs&AttrItalic != 0
315	toUnderline := to.Underline != UnderlineStyleNone
316	toBlink := to.Attrs&AttrBlink != 0
317	toRapidBlink := to.Attrs&AttrRapidBlink != 0
318	toReverse := to.Attrs&AttrReverse != 0
319	toConceal := to.Attrs&AttrConceal != 0
320	toStrikethrough := to.Attrs&AttrStrikethrough != 0
321
322	// We perform the resets first since they are single attributes and
323	// shouldn't interfere with others being set.
324
325	boldChanged := fromBold != toBold
326	faintChanged := fromFaint != toFaint
327	if boldChanged || faintChanged {
328		if fromBold && !toBold || fromFaint && !toFaint {
329			b = b.Normal()
330			boldChanged = true
331			faintChanged = true
332		}
333	}
334
335	italicChanged := fromItalic != toItalic
336	if italicChanged && !toItalic {
337		b = b.Italic(false)
338	}
339
340	underlineChanged := fromUnderline != toUnderline || from.Underline != to.Underline
341	if underlineChanged && !toUnderline {
342		b = b.Underline(false)
343	}
344
345	blinkChanged := fromBlink != toBlink
346	rapidBlinkChanged := fromRapidBlink != toRapidBlink
347	if blinkChanged || rapidBlinkChanged {
348		if fromBlink && !toBlink || fromRapidBlink && !toRapidBlink {
349			b = b.Blink(false)
350			blinkChanged = true
351			rapidBlinkChanged = true
352		}
353	}
354
355	reverseChanged := fromReverse != toReverse
356	if reverseChanged && !toReverse {
357		b = b.Reverse(false)
358	}
359
360	concealChanged := fromConceal != toConceal
361	if concealChanged && !toConceal {
362		b = b.Conceal(false)
363	}
364
365	strikethroughChanged := fromStrikethrough != toStrikethrough
366	if strikethroughChanged && !toStrikethrough {
367		b = b.Strikethrough(false)
368	}
369
370	if boldChanged && toBold {
371		b = b.Bold()
372	}
373
374	if faintChanged && toFaint {
375		b = b.Faint()
376	}
377
378	if italicChanged && toItalic {
379		b = b.Italic(true)
380	}
381
382	if underlineChanged && toUnderline && to.Underline == UnderlineStyleSingle {
383		// We only handle single underline here since others require more
384		// specific handling at the end.
385		b = b.Underline(true)
386	}
387
388	if blinkChanged && toBlink {
389		b = b.Blink(true)
390	}
391
392	if rapidBlinkChanged && toRapidBlink {
393		b = b.RapidBlink(true)
394	}
395
396	if reverseChanged && toReverse {
397		b = b.Reverse(true)
398	}
399
400	if concealChanged && toConceal {
401		b = b.Conceal(true)
402	}
403
404	if strikethroughChanged && toStrikethrough {
405		b = b.Strikethrough(true)
406	}
407
408	// Handle special underline styles.
409	if underlineChanged && toUnderline && to.Underline > UnderlineStyleSingle {
410		b = b.UnderlineStyle(to.Underline)
411	}
412
413	return b.String()
414}
415
416func colorEqual(c, o color.Color) bool {
417	if c == nil && o == nil {
418		return true
419	}
420	if c == nil || o == nil {
421		return false
422	}
423	cr, cg, cb, ca := c.RGBA()
424	or, og, ob, oa := o.RGBA()
425	return cr == or && cg == og && cb == ob && ca == oa
426}
427
428// IsZero returns true if the style is empty.
429func (s *Style) IsZero() bool {
430	return *s == Style{}
431}
432
433// ConvertStyle converts a style to respect the given color profile.
434func ConvertStyle(s Style, p colorprofile.Profile) Style {
435	switch p {
436	case colorprofile.TrueColor:
437		return s
438	case colorprofile.ANSI, colorprofile.ANSI256:
439	case colorprofile.Ascii:
440		s.Fg = nil
441		s.Bg = nil
442		s.UnderlineColor = nil
443	case colorprofile.NoTTY:
444		return Style{}
445	}
446
447	if s.Fg != nil {
448		s.Fg = p.Convert(s.Fg)
449	}
450	if s.Bg != nil {
451		s.Bg = p.Convert(s.Bg)
452	}
453	if s.UnderlineColor != nil {
454		s.UnderlineColor = p.Convert(s.UnderlineColor)
455	}
456	return s
457}
458
459// ConvertLink converts a hyperlink to respect the given color profile.
460func ConvertLink(h Link, p colorprofile.Profile) Link {
461	if p == colorprofile.NoTTY {
462		return Link{}
463	}
464	return h
465}