1package ansi
2
3// Span identifies a changed horizontal range in a logical frame row.
4type Span struct {
5 Y int
6 X int
7 Width int
8}
9
10// Scroll identifies a safe full-width upward scroll.
11type Scroll struct {
12 Y int
13 Height int
14 Count int
15}
16
17// DeltaPlan describes how to advance a committed frame to the candidate frame.
18// Snapshot requests replacement of the committed frame; Spans, when present,
19// are the canonical ranges to encode before that replacement.
20type DeltaPlan struct {
21 Snapshot bool
22 Scroll Scroll
23 Spans []Span
24}
25
26// DeltaCandidate holds a plan and owns a source frame snapshot when the plan
27// has work to commit. Empty candidates are true no-ops.
28type DeltaCandidate struct {
29 Plan DeltaPlan
30 frame Frame
31}
32
33func newDeltaCandidate(frame Frame, plan DeltaPlan) DeltaCandidate {
34 candidate := DeltaCandidate{Plan: plan}
35 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
36 candidate.frame = frame.Clone()
37 }
38 return candidate
39}
40
41// PlanDelta prepares a non-mutating update from committed to frame.
42func PlanDelta(frame Frame, damage []Damage, committed Frame, reset bool) (DeltaCandidate, error) {
43 if err := frame.Validate(); err != nil {
44 return DeltaCandidate{}, err
45 }
46
47 if reset || frame.Width != committed.Width || frame.Height != committed.Height {
48 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
49 }
50 if err := committed.Validate(); err != nil {
51 return DeltaCandidate{}, err
52 }
53
54 if len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
55 return newDeltaCandidate(frame, planSingleDamage(frame, damage[0])), nil
56 }
57
58 if needsFull(damage) {
59 dirty, full := buildDirtyLinePlan(frame, committed)
60 if len(dirty) > 0 || full {
61 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: dirty}), nil
62 }
63 return DeltaCandidate{}, nil
64 }
65
66 var plan DeltaPlan
67 var skip *Damage
68 if scroll, ok := findSafeScroll(frame, damage); ok && canApplyScrollAgainst(frame, scroll, damage, committed) {
69 plan.Scroll = Scroll{Y: scroll.Y, Height: scroll.Height, Count: scroll.Count}
70 skip = &scroll
71 } else if hasScrollDamage(damage) {
72 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
73 }
74
75 var spans []Span
76 var full bool
77 if len(damage) == 0 {
78 spans, full = buildDirtyLinePlan(frame, committed)
79 } else {
80 spans, full = buildDamagePlan(frame, damage, skip)
81 }
82 if full {
83 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
84 }
85 if deltaCostsSnapshot(frame, spans, plan.Scroll.Height != 0) {
86 if plan.Scroll.Height != 0 {
87 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
88 }
89 return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: spans}), nil
90 }
91 plan.Spans = spans
92 return newDeltaCandidate(frame, plan), nil
93}
94
95func planSingleDamage(frame Frame, d Damage) DeltaPlan {
96 spans, full := buildSingleDamageSpans(frame, d)
97 if full {
98 return DeltaPlan{Snapshot: true}
99 }
100 plan := DeltaPlan{Spans: spans}
101 if deltaCostsSnapshot(frame, spans, false) {
102 plan.Snapshot = true
103 }
104 return plan
105}
106
107func buildDirtyLinePlan(frame, committed Frame) ([]Span, bool) {
108 var spans []Span
109 for y := range frame.Height {
110 frameRow := frame.Row(y)
111 committedRow := committed.Row(y)
112 dirty := false
113 for x := range frame.Width {
114 if !frameRow[x].Equal(committedRow[x]) {
115 dirty = true
116 break
117 }
118 }
119 if dirty {
120 if len(spans) == maxPlannedDamageSpans {
121 return nil, true
122 }
123 spans = append(spans, Span{Y: y, Width: frame.Width})
124 }
125 }
126 return spans, false
127}
128
129func deltaCostsSnapshot(frame Frame, spans []Span, hasScroll bool) bool {
130 if !hasScroll && frame.Height > 1 && len(spans) == 1 {
131 return false
132 }
133
134 var deltaCost int64
135 if hasScroll {
136 deltaCost = 6
137 }
138 for _, span := range spans {
139 deltaCost += 12 + int64(span.Width)
140 }
141
142 minimumSnapshotCost := int64(frame.Height) * (12 + int64(frame.Width))
143 if deltaCost < minimumSnapshotCost {
144 return false
145 }
146 maximumSnapshotCost := int64(frame.Height) * (8 + 5*int64(frame.Width))
147 if deltaCost >= maximumSnapshotCost {
148 return true
149 }
150 if !hasScroll && fullWidthRows(frame, spans) {
151 return true
152 }
153
154 for _, span := range spans {
155 deltaCost += 4 * int64(styleRunCount(frame.Row(span.Y)[span.X:span.X+span.Width])-1)
156 }
157 var snapshotCost int64
158 for y := range frame.Height {
159 snapshotCost += spanCost(frame, y, 0, frame.Width)
160 }
161 return deltaCost >= snapshotCost
162}
163
164func fullWidthRows(frame Frame, spans []Span) bool {
165 if len(spans) != frame.Height {
166 return false
167 }
168 for y, span := range spans {
169 if span.Y != y || span.X != 0 || span.Width != frame.Width {
170 return false
171 }
172 }
173 return true
174}
175
176func spanCost(frame Frame, y, x, width int) int64 {
177 return 8 + int64(width) + 4*int64(styleRunCount(frame.Row(y)[x:x+width]))
178}
179
180func styleRunCount(cells []Cell) int {
181 if len(cells) == 0 {
182 return 0
183 }
184 runs := 1
185 for i := 1; i < len(cells); i++ {
186 if cells[i-1].Style != cells[i].Style && !cells[i-1].Style.Equal(cells[i].Style) {
187 runs++
188 }
189 }
190 return runs
191}
192
193// Commit advances dst to the frame snapshot owned by the candidate.
194func (c DeltaCandidate) Commit(dst *Frame) {
195 if !c.Plan.Snapshot && c.Plan.Scroll.Height == 0 && len(c.Plan.Spans) == 0 {
196 return
197 }
198 if c.Plan.Snapshot || dst.Width != c.frame.Width || dst.Height != c.frame.Height {
199 replaceFrame(dst, c.frame)
200 return
201 }
202 if c.Plan.Scroll.Height != 0 {
203 scroll := c.Plan.Scroll
204 dst.ScrollUp(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
205 }
206 for _, span := range c.Plan.Spans {
207 dstRow := dst.Row(span.Y)
208 srcRow := c.frame.Row(span.Y)
209 copy(dstRow[span.X:span.X+span.Width], srcRow[span.X:span.X+span.Width])
210 }
211}
212
213func replaceFrame(dst *Frame, src Frame) {
214 dst.Replace(src)
215}