1package ansi
2
3import (
4 "cmp"
5 "slices"
6)
7
8const maxPlannedDamageSpans = 4096
9
10// buildDamagePlan creates the bounded, canonical view of text and clear damage
11// used for terminal emission. The source damage remains untouched because the
12// scroll and shadow paths need its original structural information.
13func buildDamagePlan(frame CellSource, damage []Damage, skip *Damage) ([]Span, bool) {
14 if len(damage) == 1 {
15 d := damage[0]
16 if skip != nil && sameDamage(d, *skip) {
17 return nil, false
18 }
19 if d.Kind != DamageText && d.Kind != DamageClear {
20 return nil, false
21 }
22 return buildSingleDamageSpans(frame, d)
23 }
24
25 spans := make([]Span, 0)
26 for _, d := range damage {
27 if skip != nil && sameDamage(d, *skip) {
28 continue
29 }
30 if d.Kind != DamageText && d.Kind != DamageClear {
31 continue
32 }
33 x, y, width, height, ok := clampRect(frame, d.X, d.Y, d.Width, d.Height)
34 if !ok {
35 continue
36 }
37 for row := y; row < y+height; row++ {
38 if len(spans) == maxPlannedDamageSpans {
39 return nil, true
40 }
41 spans = append(spans, wideAlignedSpan(frame, row, x, width))
42 }
43 }
44
45 slices.SortFunc(spans, func(a, b Span) int {
46 if c := cmp.Compare(a.Y, b.Y); c != 0 {
47 return c
48 }
49 return cmp.Compare(a.X, b.X)
50 })
51
52 return mergeDamageSpans(spans), false
53}
54
55func buildSingleDamageSpans(frame CellSource, d Damage) ([]Span, bool) {
56 x, y, width, height, ok := clampRect(frame, d.X, d.Y, d.Width, d.Height)
57 if !ok {
58 return nil, false
59 }
60 if height > maxPlannedDamageSpans {
61 return nil, true
62 }
63 spans := make([]Span, height)
64 for row := range height {
65 spans[row] = wideAlignedSpan(frame, y+row, x, width)
66 }
67 return spans, false
68}
69
70// wideAlignedSpan widens a clamped span so it never splits a wide rune.
71// Damage can name only one half of a pair, for example when text damage was
72// recorded before a scroll moved a wide rune under it. Writing from the right
73// half would erase the rune on the client, and the shadow must hold whole pairs.
74func wideAlignedSpan(frame CellSource, y, x, width int) Span {
75 if x > 0 && frame.Cell(x, y).Continuation {
76 x--
77 width++
78 }
79 if end := x + width; end < frame.Columns() && frame.Cell(end, y).Continuation {
80 width++
81 }
82 return Span{Y: y, X: x, Width: width}
83}
84
85func mergeDamageSpans(spans []Span) []Span {
86 if len(spans) < 2 {
87 return spans
88 }
89
90 out := spans[:0]
91 for _, span := range spans {
92 if len(out) == 0 {
93 out = append(out, span)
94 continue
95 }
96 last := &out[len(out)-1]
97 // Both endpoints are bounded by frame.Width after clampRect, so these
98 // additions cannot overflow. The comparison includes adjacency.
99 lastEnd := last.X + last.Width
100 spanEnd := span.X + span.Width
101 if span.Y != last.Y || span.X > lastEnd {
102 out = append(out, span)
103 continue
104 }
105 if spanEnd > lastEnd {
106 last.Width = spanEnd - last.X
107 }
108 }
109 return out
110}