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 CellSource, plan DeltaPlan) DeltaCandidate {
34 candidate := DeltaCandidate{Plan: plan}
35 if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
36 candidate.frame = cloneCellSource(frame)
37 }
38 return candidate
39}
40
41// PlanDelta prepares a non-mutating update from committed to frame.
42func PlanDelta(frame CellSource, damage []Damage, committed Frame, reset bool) (DeltaCandidate, error) {
43 if err := validateCellSource(frame); err != nil {
44 return DeltaCandidate{}, err
45 }
46
47 if reset || frame.Columns() != committed.Columns() || frame.Rows() != committed.Rows() {
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 CellSource, 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 CellSource) ([]Span, bool) {
108 var spans []Span
109 for y := range frame.Rows() {
110 dirty := false
111 for x := range frame.Columns() {
112 if !frame.Cell(x, y).Equal(committed.Cell(x, y)) {
113 dirty = true
114 break
115 }
116 }
117 if dirty {
118 if len(spans) == maxPlannedDamageSpans {
119 return nil, true
120 }
121 spans = append(spans, Span{Y: y, Width: frame.Columns()})
122 }
123 }
124 return spans, false
125}
126
127func deltaCostsSnapshot(frame CellSource, spans []Span, hasScroll bool) bool {
128 if !hasScroll && frame.Rows() > 1 && len(spans) == 1 {
129 return false
130 }
131
132 var deltaCost int64
133 if hasScroll {
134 deltaCost = 6
135 }
136 for _, span := range spans {
137 deltaCost += 12 + int64(span.Width)
138 }
139
140 minimumSnapshotCost := int64(frame.Rows()) * (12 + int64(frame.Columns()))
141 if deltaCost < minimumSnapshotCost {
142 return false
143 }
144 maximumSnapshotCost := int64(frame.Rows()) * (8 + 5*int64(frame.Columns()))
145 if deltaCost >= maximumSnapshotCost {
146 return true
147 }
148 if !hasScroll && fullWidthRows(frame, spans) {
149 return true
150 }
151
152 for _, span := range spans {
153 deltaCost += 4 * int64(styleRunCountIn(frame, span.Y, span.X, span.Width)-1)
154 }
155 var snapshotCost int64
156 for y := range frame.Rows() {
157 snapshotCost += spanCost(frame, y, 0, frame.Columns())
158 }
159 return deltaCost >= snapshotCost
160}
161
162func fullWidthRows(frame CellSource, spans []Span) bool {
163 if len(spans) != frame.Rows() {
164 return false
165 }
166 for y, span := range spans {
167 if span.Y != y || span.X != 0 || span.Width != frame.Columns() {
168 return false
169 }
170 }
171 return true
172}
173
174func spanCost(frame CellSource, y, x, width int) int64 {
175 return 8 + int64(width) + 4*int64(styleRunCountIn(frame, y, x, width))
176}
177
178func styleRunCountIn(frame CellSource, y, x, width int) int {
179 if width == 0 {
180 return 0
181 }
182 runs := 1
183 previous := frame.Cell(x, y).Style
184 for column := x + 1; column < x+width; column++ {
185 style := frame.Cell(column, y).Style
186 if previous != style && !previous.Equal(style) {
187 runs++
188 }
189 previous = style
190 }
191 return runs
192}
193
194// Commit advances dst to the frame snapshot owned by the candidate.
195func (c DeltaCandidate) Commit(dst *Frame) {
196 if !c.Plan.Snapshot && c.Plan.Scroll.Height == 0 && len(c.Plan.Spans) == 0 {
197 return
198 }
199 if c.Plan.Snapshot || dst.Width != c.frame.Width || dst.Height != c.frame.Height {
200 replaceFrame(dst, c.frame)
201 return
202 }
203 if c.Plan.Scroll.Height != 0 {
204 scroll := c.Plan.Scroll
205 dst.ScrollUp(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
206 }
207 for _, span := range c.Plan.Spans {
208 for x := span.X; x < span.X+span.Width; x++ {
209 dst.Set(x, span.Y, c.frame.Cell(x, span.Y))
210 }
211 }
212}
213
214func replaceFrame(dst *Frame, src Frame) {
215 dst.Replace(src)
216}