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